Contents
Chapter 2

Tour

This chapter and the ones that follow give a programmer’s tour of Python: syntax and the built-in numbers, strings, and None here, then containers, control flow, functions, modules, classes, static typing, class attributes, and object cleanup. It assumes you have programming experience. Find supplementary information in the official language documentation.

Scripting vs. Programming

Python exists to improve your productivity. The language aims to aid you as much as possible. It tries to hinder you as little as possible. It does not impose arbitrary rules or force a particular set of features.

People often call Python a scripting language, but scripting languages tend to limit you, especially in the range of problems they can solve. Python is a programming language that also supports scripting. It is marvelous for scripting, and you may replace all your batch files, shell scripts, and simple programs with Python scripts.

How to Read the Examples

The # denotes a comment that goes until the end of the line, just like C++ and Java // comments. The Examples explains the filename first line and the #: output markers.

Indentation and Blocks

Python is clean to write. Your own code stays easy to read long after you’ve written it. Indentation determines how statements group into blocks. This script runs with python if.py:

# if.py

response = "yes"
if response == "yes":
    print("affirmative")
    val = 1
#: affirmative
print("continuing...")
#: continuing...
print(val)
#: 1

A C/C++ if requires parentheses around the conditional. Python makes them optional.

The conditional clause ends with a colon. A group of indented statements follows: the “then” part of the if statement. The print() function sends its argument to standard output. The next line assigns to a variable named val. The next statement returns to the left margin, and that return ends the if.

An indented block groups statements without creating a scope, so val, assigned inside the if, stays visible afterward, unlike a variable declared inside braces in C++ or Java. New scopes come from functions, classes, modules, and comprehensions, never from an if or a for block. Python binds a name only when the assignment runs: with any answer other than "yes", val = 1 never executes, and print(val) raises a NameError.

# unbound_val.py

response = "no"
if response == "yes":
    val = 1
try:
    print(val)  # type: ignore
except NameError as e:
    print(e)
#: name 'val' is not defined

The if never ran, so val was never bound. Indentation shows where the assignment sits, not whether it runs. ty sees that nothing ever defines val and reports an error on that line, so # type: ignore tells it the mistake is deliberate.

Indenting can nest as deeply as you like. Four spaces per level is the convention, and mixing tabs and spaces inconsistently inside one block raises a TabError. C++ and Java programmers debate where braces go. In Python the indentation is the structure, so the language settles the question and taste plays no part. Python code from any two authors therefore lines up the same way, and that sameness is one of the main reasons for Python’s consistent readability.

A statement ends with its line, so it needs no terminating semicolon. A semicolon’s one job is to separate two statements that share a line.

Variables and References

A variable in Python is a name bound to an object, not a box that holds a value. Assignment binds a name. It does not copy. You never declare a variable’s type, and one name can bind to objects of different types over its life. That freedom is dynamic typing. Python also has a full static type system layered on top, and this book uses it from Static Types onward.

# references.py

x = 10  # x names an int
x = "ten"  # The same name now binds to a str
a = [1, 2, 3]
b = a  # b binds to the same list, not a copy
b.append(4)
print(a)  # The same object: a and b
#: [1, 2, 3, 4]
print(a is b)  # Identical objects
#: True
c = a[:]  # Copies the list, not its contents
print(a is c, a == c)  # Different object, equal value
#: False True
nested = [[1], [2, 3]]
shallow = nested[:]
shallow[1].append(99)
print(nested)  # The inner list is shared
#: [[1], [2, 3, 99]]

Use == to ask whether two objects have equal values. Use is to ask whether two names refer to the same object. Reserve is for None and other singletons. a[:] is a shallow copy: it duplicates the outer list but not the objects inside it. nested and shallow still share the same inner list, so shallow[1].append(99) changes what nested sees too.

You can assign several names at once, so a swap needs no temporary:

# multiple_assignment.py

a, b = 1, 2
a, b = b, a  # Swap without a temporary
print(a, b)
#: 2 1
first, *rest = [10, 20, 30, 40]
print(first, rest)
#: 10 [20, 30, 40]

*rest collects whatever remains. Containers covers the general form.

Numbers, strings, and tuples are immutable: operations produce new objects rather than changing the original. Lists, dictionaries, and sets are mutable. Mutability decides whether another name sees a change, as a and b did in references.py.

Numbers and Arithmetic

Integers have unlimited precision, so they cannot overflow. Underscores group digits for readability, so 10_000_000 is the same literal as 10000000. Floating point is the usual IEEE double. The operators are what you expect, with two worth noting: / on integers always produces a float, and // is floor division (divide, then round down to a whole number). The result’s type follows the operands, so 7.0 // 2 is 3.0. Floor division rounds toward negative infinity, not toward zero, so -7 // 2 is -4 where C and Java give -3. The remainder follows from that, so -7 % 2 is 1 in Python and -1 in C. The sign of % matches the divisor.

# arithmetic.py

print(7 / 2)  # True division, always a float
#: 3.5
print(7 // 2)  # Floor division
#: 3
print(7 % 2)  # Remainder
#: 1
print(-7 // 2, -7 % 2)  # Floors, not truncates toward zero
#: -4 1
print(2 ** 10)  # Exponentiation
#: 1024
print(10 ** 30)  # A 31-digit int, no overflow
#: 1000000000000000000000000000000
print(abs(-5), round(3.14159, 2))
#: 5 3.14
total = 0
total += 5  # Augmented assignment, like other languages
print(total)
#: 5
scores = [90, 0, 71, 0, 55]
print(sum(s > 60 for s in scores))  # True counts as 1
#: 2
items = [1, 2]
alias = items
items += [3]  # In place, so alias sees it
print(alias)
#: [1, 2, 3]

Augmented assignment on a mutable object changes it in place, so every other name for it sees the change. items = items + [3] would instead build a new list and leave alias alone. For an int, both forms rebind the name, so total += 5 above behaves the way += does in any other language.

round() breaks a tie to the nearest even value, so round(0.5) is 0 and round(1.5) is 2, rather than rounding half away from zero as C does. An f-string’s format spec rounds the same way.

Python has no ++ or --. Use += 1 and -= 1. Each arithmetic operator has an augmented-assignment form: +=, -=, *=, /=, //=, %=, and **=.

A bool is a subtype of int, so True equals 1 and False equals 0. Summing a sequence of comparisons therefore counts how many are true. The argument to sum() is a generator expression, which hands over one value at a time instead of building a list first. Comprehensions covers the form.

Integers also support the bitwise and shift operators, each with a matching augmented form (&=, |=, ^=, <<=, >>=). Binary literals, which start with 0b, make the bit patterns readable:

# bitwise.py

print(bin(0b1100 & 0b1010))  # AND, bits set in both
#: 0b1000
print(bin(0b1100 | 0b1010))  # OR, bits set in either
#: 0b1110
print(bin(0b1100 ^ 0b1010))  # XOR, bits set in exactly one
#: 0b110
print(bin(~0b1100))  # NOT, inverts every bit
#: -0b1101
print(bin(1 << 4))  # Left shift, same as 1 * 2 ** 4
#: 0b10000
# Right shift, same as 48 // 2 ** 2
print(bin(0b110000 >> 2))
#: 0b1100

flags = 0
flags |= 0b0010  # Set bits with the augmented form
flags |= 0b1000
print(bin(flags))
#: 0b1010

The bin() function converts an integer to a binary string for display. Because Python integers have no fixed width, ~ has no fixed number of bits to flip. ~x produces -x - 1, the value that flipping every bit gives in two’s complement. bin() prints that as a sign followed by a magnitude, so ~0b1100 reads as -0b1101 rather than a row of ones.

Python reserves one further operator, @ (with @= to match), for matrix multiplication. The built-in numeric types do not implement it, but array libraries such as NumPy do. The same character in front of a def or a class means something else: that is decorator syntax, covered in Decorators.

Booleans, None, and Truthiness

None is Python’s single “no value” object, like null elsewhere. It is the default return value of a function that returns nothing.

You can test any object in a boolean context. Numbers are false when zero, containers are false when empty, and None is always false. Everything else is true, unless an object’s type says otherwise. That rule is truthiness, and it lets you write if items: instead of if len(items) != 0:. A type says otherwise by defining __bool__(). Without one, Python falls back to __len__(), and that fallback is why an empty container is false.

# truthiness.py

for value in [0, 1, "", "hi", [], [1], None]:
    print(repr(value), "->", bool(value))
#: 0 -> False
#: 1 -> True
#: '' -> False
#: 'hi' -> True
#: [] -> False
#: [1] -> True
#: None -> False

class Bucket:
    def __init__(self, count: int) -> None:
        self.count = count

    def __len__(self) -> int:
        return self.count

print(bool(Bucket(0)), bool(Bucket(3)))
#: False True

if not []:
    print("empty")  # An empty list is falsy
#: empty

# 'or' returns the first truthy operand
name = "" or "default"
print(name)
#: default
count = 0
print(count or 10)  # 0 is falsy, so the fallback wins
#: 10

repr() returns a value’s unambiguous representation, so the empty string shows as '' and not as blank. Bucket defines no __bool__(), so bool() falls back to its __len__(): Bucket(0) is false and Bucket(3) is true.

and and or short-circuit and return one of their operands, not a coerced boolean. x or default is a common way to supply a fallback, and it replaces every falsy x: a legitimate 0 or "" gets the fallback just as a missing value does. When zero or an empty string is a legal value, test for None instead: default if x is None else x. That is a conditional expression, covered in Control Flow.

Strings

Single or double quotes create strings. If you surround a string with double quotes, you can embed single quotes and vice versa:

# strings.py

print("That isn't a horse")
#: That isn't a horse
print('You are not a "Viking"')
#: You are not a "Viking"
print("""
You're just pounding two
coconut halves together.
""")
#:
#: You're just pounding two
#: coconut halves together.
#:
print('''
"Oh no!" He exclaimed.
"It's the blancmange!"
''')
#:
#: "Oh no!" He exclaimed.
#: "It's the blancmange!"
#:
print(r'c:\python\lib\utils')
#: c:\python\lib\utils

Python’s name comes not from the snake but from the Monty Python comedy troupe, of which the language creator Guido van Rossum is a fan. Examples often include Python-esque references.

The triple-quote syntax quotes everything, including newlines. That suits any block of literal text, such as an embedded template, a SQL query, or a chunk of HTML: you can write it out in full without escaping line breaks.

In an ordinary string, a backslash starts an escape sequence, as in C and Java: \n is a newline and \t is a tab. The r right before a string means “raw”: Python takes each backslash literally, as a single character. One limit remains. A raw string cannot end with a backslash, because even there the backslash escapes the closing quote.

Common String Operations

Strings are immutable sequences with a large set of methods. Slicing also selects a range of characters, and in tests membership:

# string_methods.py

s = "  Hello, World  "
print(s.strip())
#: Hello, World
print(s.strip().lower())
#: hello, world
print("World" in s)
#: True
print("a,b,c".split(","))
#: ['a', 'b', 'c']
print("-".join(["2024", "06", "15"]))
#: 2024-06-15
print("ababab".replace("a", "X"))
#: XbXbXb
print(s.strip()[0:5])
#: Hello

String methods return new values rather than changing the original.

f-Strings

Modern Python uses f-strings. Prefix the string with f and put expressions in curly braces. The result is readable and fast:

# fstrings.py

name = "Alice"
score = 91.5
print(f"{name} scored {score}")
#: Alice scored 91.5
print(f"{name} scored {score:.0f}%")
#: Alice scored 92%
print(f"{name!r} has {len(name)} letters")
#: 'Alice' has 5 letters
total = 7
print(f"{total = }")  # Useful for debugging
#: total = 7
print(f"|{name:>10}|{score:<8.1f}|")
#: |     Alice|91.5    |

The format spec after a colon controls width, precision, and alignment. > right-aligns and < left-aligns within the given width. A !r on the expression, as in {name!r}, formats the value with repr() instead of str().

Existing code also carries two older styles: C’s printf() syntax, as in "val: %d" % val, and the str.format() method, as in "val: {}".format(val). Both still work. str.format() shares the f-string’s format mini-language, while the % form has its own, inherited from C’s printf(). F-strings replaced them, so this book uses f-strings throughout.

t-Strings

An f-string produces a finished str, deciding how each value becomes text before anything else sees it. A t-string produces a Template instead, which keeps the literal pieces and the interpolated values apart for a consumer to assemble.

The reason to care is safety. A consumer that receives the parts separately knows which text came from the program and which came from a value, so it can quote, escape, or reject the values before they become part of the result.

The parts look like this:

# tstrings.py
from string.templatelib import Interpolation, Template

name = "Alice"
score = 91.5
message: Template = t"{name} scored {score:.0f}%"
print(message.strings)
#: ('', ' scored ', '%')
print([piece.expression
       for piece in message.interpolations])
#: ['name', 'score']

def shout(template: Template) -> str:
    parts: list[str] = []
    for piece in template:
        if isinstance(piece, Interpolation):
            parts.append(
                format(piece.value, piece.format_spec))
        else:
            parts.append(piece.upper())
    return "".join(parts)

print(shout(message))
#: Alice SCORED 92%

def safe(template: Template) -> str:
    parts: list[str] = []
    for piece in template:
        if isinstance(piece, Interpolation):
            value = str(piece.value)
            if "'" in value:
                raise ValueError(
                    f"unsafe value: {value!r}")
            parts.append(value)
        else:
            parts.append(piece)
    return "".join(parts)

print(safe(t"Hello, {name}"))
#: Hello, Alice
trouble = "Bob'; rm -rf /"
try:
    safe(t"Hello, {trouble}")
except ValueError as e:
    print(e)
#: unsafe value: "Bob'; rm -rf /"

Iterating a Template produces the pieces in order, each either a str the author typed or an Interpolation carrying a value. An Interpolation also remembers the source text of the expression that produced it, and piece.expression reports that text. Collecting every piece.expression above uses a list comprehension, which has the same kind of for clause as the generator expression in arithmetic.py but builds a list; Comprehensions covers the general form. Iteration skips empty literal strings, so the leading '' in message.strings does not reach the loop. That skipping is why a consumer cannot assume that literals and interpolations alternate. shout() uppercases the literal text and leaves the interpolated values in their original case. No amount of work on a finished f-string could do that reliably, because the finished string no longer says which characters came from where. safe() puts that separation to work: it passes the literal text through unchanged, and it rejects an interpolated value that contains a single quote, the way trouble does above. An f-string would have finished assembling the result, quote and all, before any code had a chance to object. Uppercasing and rejecting are small demonstrations; Composite and Interpreter builds a full query from the parts the same way.

Keep using f-strings for ordinary output. Use a t-string only when a consumer must inspect, escape, or reject the interpolated values before they become part of the result, the way safe() does above.

Naming Conventions

Use snake_case for variables, functions, methods, and file names: lower case with words separated by underscores, as in this_is_snake_case.

For a constant, use all uppercase letters, as in THIS_IS_A_CONSTANT.

Class names are CapWords (Pascal cased): every word, including the first, begins with a capital letter, and no underscores separate them. For example: ThisIsMyClass.

When users call a class the way they call a function, that class may use snake_case instead. The standard library names contextlib.suppress, functools.partial, and the builtins property and staticmethod that way. Name every other class CapWords.

PEP 8 covers style issues. Tools such as ruff point out violations and fix many of them automatically.

Exercises

  1. In references.py, add a line after c = a[:] that appends 99 to c. Print a and c and confirm only c changed, then explain why b.append(4) earlier did change what a sees, but appending to c does not.
  2. In truthiness.py, add an empty dictionary {} and a dictionary with one entry to the list of test values. Predict what bool() reports for each before running it, then check your prediction.
  3. In fstrings.py, add a line that formats score with two decimal places instead of zero, using {score:.2f} in place of {score:.0f}%, and a second line using the debug specifier, f"{score = }".
  4. arithmetic.py defines total and bitwise.py defines flags. Rename them to totalSum and flagBits, then to TOTAL_SUM and FLAG_BITS. Every version runs. Using Naming Conventions, say what each form signals to a reader who did not write the code, and which of the three a linter would flag.
  5. In tstrings.py, write a second consumer, quoted(template), that wraps every interpolated value in single quotes and leaves the literal text alone, then print quoted(message). Explain why you cannot post-process an f-string the same way.
  6. Before running anything, write down what C or Java would print for -9 / 4 and -9 % 4 using integer math, then what Python prints for -9 // 4 and -9 % 4. Run print(-9 // 4, -9 % 4) and check. State the rule that predicts the sign of the result of %.