In
references.py, add a line afterc = a[:]that appends99toc. Printaandcand confirm thatcchanged andastill holds[1, 2, 3, 4], then explain whyb.append(4)earlier did change whatasees, but appending tocdoes not.
Variables
and References shows that assignment copies a reference, not
the list. A slice builds a new list object, so a
and c stop sharing. Check which of b
and c names the same list as a before
you explain the difference.
# exercise_1.py
a = [1, 2, 3]
b = a # b is another name for the same list
b.append(4)
c = a[:] # A shallow copy: a new list, same values
c.append(99)
print(a, c)
#: [1, 2, 3, 4] [1, 2, 3, 4, 99]b.append(4) changes a too, because
b and a name the same list object.
c = a[:] makes a new list with the same elements,
so c.append(99) changes c and leaves
a alone. Slicing copies. Assignment does not.
In
truthiness.py, add an empty dictionary{}and a dictionary with one entry to the list of test values. Predict whatbool()reports for each before running it, then check your prediction.
Booleans,
None, and Truthiness lists which values bool()
reports as false. Add the two dictionaries to the loop’s list
and write your predictions down first. Then ask whether the rule
you see for lists and strings extends to a
dict.
# exercise_2.py
for value in [0, 1, "", "hi", [], [1], None, {}, {"k": 1}]:
print(repr(value), "->", bool(value))
#: 0 -> False
#: 1 -> True
#: '' -> False
#: 'hi' -> True
#: [] -> False
#: [1] -> True
#: None -> False
#: {} -> False
#: {'k': 1} -> TrueAn empty dictionary is falsy, the same as an empty list or an empty string. A dictionary with one or more entries is truthy. The rule is the same for every container: falsy when empty, truthy otherwise.
In
fstrings.py, add a line that formatsscorewith two decimal places instead of zero, using{score:.2f}in place of{score:.0f}%, and a second line using the debug specifier,f"{score = }".
f-Strings
covers the format spec after the colon in a replacement field.
Change the digit after the . in .0f to
set the precision. A trailing = inside the braces
makes the field print its own source text before the value.
# exercise_3.py
name = "Alice"
score = 91.5
print(f"{name} scored {score:.2f}")
#: Alice scored 91.50
print(f"{score = }")
#: score = 91.5.2f always shows two digits after the decimal
point, a trailing zero included. {score = } prints
both the expression’s source text and its value, so a quick
debugging print needs no separate
print("score", score).
augmented.pydefinestotalandbitwise.pydefinesflags. Rename them tototalSumandflagBits, then toTOTAL_SUMandFLAG_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 flags.
Naming
Conventions says what each casing form tells a reader.
Rename the variables in both styles and run each version to
confirm the interpreter does not care. Then run
ruff check on the camelCase file and see which rule
code it reports.
The camelCase versions of total and
flags:
# exercise_4.py
totalSum = 0 # noqa: N816
totalSum += 5 # noqa: N816
flagBits = 0b0010 # noqa: N816
flagBits |= 0b1000 # noqa: N816
print(totalSum, bin(flagBits))
#: 5 0b1010And the all-uppercase versions:
# exercise_4_constants.py
TOTAL_SUM = 0
TOTAL_SUM += 5
FLAG_BITS = 0b0010
FLAG_BITS |= 0b1000
print(TOTAL_SUM, bin(FLAG_BITS))
#: 5 0b1010All three forms run, since Python does not enforce a naming
convention at the language level. What differs is what a reader
infers. total and flags say “an
ordinary variable that changes,” which is what both of these
are. TOTAL_SUM and FLAG_BITS say “a
constant, fixed for the life of the program,” yet the second
line of exercise_4_constants.py changes
TOTAL_SUM. Neither Python nor the linter objects,
so the name misleads every reader who trusts it.
totalSum and flagBits say nothing
about the value. They say the author came from Java or
JavaScript.
Only the camelCase form breaks Naming
Conventions, and it is the only one a linter flags: ruff’s
PEP 8 checks report N816 for a mixed-case global.
The uppercase form is legal style, merely a false claim about
the value. CapWords stays reserved for class names.
Template consumerIn
tstrings.py, write a third consumer,quoted(template), that wraps every interpolated value in single quotes and leaves the literal text alone, then printquoted(message). Explain why you cannot post-process an f-string the same way.
t-Strings
shows a Template as a sequence of literal strings
and Interpolation objects. Iterate over the
template, test each piece with isinstance(), and
build the result from the pieces. For the explanation, consider
what an f-string has done by the time you receive its
result.
# The shape of exercise_5.py
from string.templatelib import Interpolation, Template
def quoted(template: Template) -> str:
...If you build each value with str(piece.value),
the way the chapter’s safe() does, the output reads
'Alice' scored '91.5'%. The value drops the
template’s :.0f, because str()
receives the value alone and the Interpolation
keeps the spec separately, in piece.format_spec.
The solution passes the value and its spec to
format(), as shout() does, so
score still prints as '92'.
# exercise_5.py
from string.templatelib import Interpolation, Template
name = "Alice"
score = 91.5
message: Template = t"{name} scored {score:.0f}%"
def quoted(template: Template) -> str:
parts: list[str] = []
for piece in template:
if isinstance(piece, Interpolation):
value = format(piece.value, piece.format_spec)
parts.append(f"'{value}'")
else:
parts.append(piece)
return "".join(parts)
print(quoted(message))
#: 'Alice' scored '92'%Tell values from literal text.
quoted() is shout() with the two
branches swapped over: the Interpolation branch is
the one that changes something, and the literal branch passes
its text through. The isinstance() test does all
the work. Each piece arrives labelled as text the author typed
or as a value the program supplied, so deciding what to do with
each is a two-line if rather than a parsing
problem.
You cannot post-process an f-string this way, because the
string it produces carries no label.
f"{name} scored {score:.0f}%" evaluates to the
single string Alice scored 92%, and nothing in that
string records that Alice came from a variable and
scored came from the source. A post-processor has
only the characters, so it must guess which spans to quote by
matching them against the values. The guess fails as soon as a
literal looks like a value: with name = "scored",
the finished string reads scored scored 92%, and
nothing in it says which of the two words is the value.
That failed guess is the argument for Template
in one example. Quoting is a harmless demonstration, but the
same reasoning covers escaping a value before it enters SQL or
HTML, where a wrong guess is a security hole rather than a
typo.
Before running anything, write down what C or Java prints for
-9 / 4and-9 % 4using integer math, then what Python prints for-9 // 4and-9 % 4. Runprint(-9 // 4, -9 % 4)and check. State the rule that predicts the sign of the result of%.
Numbers
and Arithmetic describes // and %
on integers. Work the C or Java answer by truncating toward
zero, then the Python answer by flooring. Use the identity
relating //, %, and the divisor to see
what the remainder must be.
# exercise_6.py
print(-9 // 4, -9 % 4)
#: -3 3C and Java truncate integer division toward zero, so their
integer -9 / 4 is -2 and
-9 % 4 is -1. Python floors toward
negative infinity, so -9 // 4 is -3.
The identity a == (a // b) * b + a % b then forces
the remainder to 3. The rule is that the result of
% takes the sign of the divisor. With a positive
divisor the remainder is nonnegative, which is why
index % len(items) wraps cleanly in either
direction.