Classes II: Inheritance, Special Methods and Modules
Extend the Rover expedition with related roles, Python operation protocols and real modules. Trace initialization, receivers, imported bindings and program entry, then inspect the execution environment.
By the end of this lecture you should be able to
- Trace subclass initialization, super and the actual method receiver
- Choose between inheritance and composition using supported behavior
- Define representation, addition, equality and container operations
- Explain module objects, imported references and guarded entry points
- Inspect the interpreter and module origin and distinguish two caches
Contents
Continue the expedition
L9’s Rover stores coordinates, coins and collected items. We now add scouting and cargo roles while keeping common operations. The same reporting loop should work for every role. We then give positions and backpacks useful Python operations and move reusable definitions into modules.
Download the classroom project · Open the visual demonstration
The folders 01_roles/, 02_protocols/ and 03_expedition/ are successive stages. Run python main.py inside the chosen folder with Python3.13. Only the standard library is needed. The HTML/JavaScript demonstration displays the corresponding state changes. Minimal examples isolate a mechanism; the complete project retains coordinates, coins and backpacks.
I. Extend roles through inheritance
Start with an inherited operation
A subclass can find and reuse attributes through its base classes. In class ScoutRover(Rover):, Rover is the base and ScoutRover is the subclass. Begin with an empty subclass:
class Rover:
def __init__(self, x, y):
self.x, self.y = x, y
def move(self, dx, dy):
self.x += dx
self.y += dy
class ScoutRover(Rover):
pass
s = ScoutRover(3, 4)
s.move(1, 0)
print(s.x, s.y)
print(isinstance(s, Rover))
4 4
True
The inherited initializer establishes fields on s, and the inherited move updates s. The function is found on Rover while the receiver remains the ScoutRover instance. isinstance(s, Rover) accepts a subclass instance; type(s) is Rover would require the exact class.
Separate method lookup from instance storage. No parent instance is copied into the new object. These examples use ordinary methods; descriptors and customized attribute access add further rules.
Coordinate initialization when overriding
Defining a subclass initializer changes which initializer is called. It does not automatically invoke the base initializer. This candidate establishes a scan range but omits the coordinate assignments:
class Rover:
def __init__(self, x, y):
self.x = x
self.y = y
class ScoutRover(Rover):
def __init__(self, x, y, scan_range):
self.scan_range = scan_range
s = ScoutRover(3, 4, 2)
print(s.x, s.y, s.scan_range)
AttributeError: 'ScoutRover' object has no attribute 'x'
Call the existing initialization and then establish the extra field:
class Rover:
def __init__(self, x, y):
self.x = x
self.y = y
class ScoutRover(Rover):
def __init__(self, x, y, scan_range):
super().__init__(x, y)
self.scan_range = scan_range
s = ScoutRover(3, 4, 2)
print(s.x, s.y, s.scan_range)
3 4 2
The slide sequence follows one instance throughout. ScoutRover initialization starts with an empty object. Calling super enters Rover’s initializer with the same self; x and y are assigned. After that call returns None, the subclass stores scan_range and returns None.
super() continues along the actual class’s method resolution order after the current class. Here that order is ScoutRover, Rover, object. Multiple inheritance can place another class next. Initialize required state before code that depends on it; instance attributes can also be created by other methods.
An inherited method keeps its receiver
Overriding supplies another implementation of an inherited method name. Here report is reused and symbol is overridden:
class Rover:
def symbol(self):
return "R"
def report(self):
return self.symbol()
class ScoutRover(Rover):
def symbol(self):
return "S"
s = ScoutRover()
print(s.report())
S
Lookup finds Rover.report and executes it with s as self. Its self.symbol() access selects ScoutRover.symbol and obtains S. Entering a base implementation does not replace the receiver. An explicit Rover.report(s) still passes s and gives the same result.
A subclass can also extend the result of a reused method:
class Rover:
def describe(self):
return "at (3, 4)"
class ScoutRover(Rover):
def describe(self):
base = super().describe()
return base + ", scan range 2"
s = ScoutRover()
print(s.describe())
at (3, 4), scan range 2
One interface across different implementations
The caller can use a common operation without branching on every role:
class Rover:
def symbol(self):
return "R"
def report(self):
return self.symbol()
class ScoutRover(Rover):
def symbol(self):
return "S"
class CargoRover(Rover):
def symbol(self):
return "C"
team = [Rover(), ScoutRover(), CargoRover()]
for rover in team:
print(rover.report())
R
S
C
The shared loop obtains R, S and C through the same report call. A subclass should preserve the behavior its clients rely on; a real-world classification alone does not establish that compatibility.
A common custom base class is not required for every compatible call:
class ScoutRover:
def report(self):
return "S: ready"
class Beacon:
def report(self):
return "B: online"
for device in [ScoutRover(), Beacon()]:
print(device.report())
print(issubclass(Beacon, ScoutRover))
S: ready
B: online
False
Both objects support a no-argument report returning a description. Duck typing relies on usable behavior, including compatible parameters, results and side effects.
Composition stores and uses another object. The full Rover initializes self.bag = Backpack(). A scout is a kind of Rover; a backpack is a component a rover owns. Reusing a component does not require claiming its complete interface through inheritance.
II. Support familiar Python operations
Give object display a useful meaning
Both __str__ and __repr__ must return strings. Use a readable display for str and an informative representation for repr. Lists display the repr of each element; the !r conversion requests repr for an inserted value.
class Rover:
def __init__(self, name):
self.name = name
def __str__(self):
return "Rover " + self.name
def __repr__(self):
return f"Rover({self.name!r})"
s = Rover("Ada")
print(s)
print(repr(s))
print([s])
Rover Ada
Rover('Ada')
[Rover('Ada')]
str can fall back to repr when needed. The reverse is not a corresponding guarantee. A representation may aim to be unambiguous or reconstructible, but it is not permission to evaluate unknown text. In the full project, str calls describe, retaining role-specific dispatch.
Position addition: inputs, result and mutation
Coordinate fields do not by themselves define addition. Our operation adds coordinates component by component and returns a new Position, preserving the inputs:
class Position:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
if not isinstance(other, Position):
return NotImplemented
x = self.x + other.x
y = self.y + other.y
return Position(x, y)
def __repr__(self):
return f"Position({self.x}, {self.y})"
p = Position(3, 4)
q = p + Position(1, 0)
print(p, q, p is q)
Position(3, 4) Position(4, 4) False
An unsupported operand produces the NotImplemented sentinel, allowing operator dispatch to continue. If no applicable arithmetic path supports the operands, the expression normally raises TypeError. The sentinel and the exception NotImplementedError have different roles.
class Position:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
if not isinstance(other, Position):
return NotImplemented
x = self.x + other.x
y = self.y + other.y
return Position(x, y)
def __repr__(self):
return f"Position({self.x}, {self.y})"
p = Position(3, 4)
print(p.__add__("east") is NotImplemented)
print(p + "east")
True
TypeError: unsupported operand type(s) for +: 'Position' and 'str'
Implicit special operations generally use type-level lookup and inheritance, rather than any same-named instance field. Reflected dispatch, right-subtype priority and augmented assignment are covered in the optional material. Creating a new result is this operation’s contract, not a universal rule for addition.
Coordinate equality and object identity
class Position:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Position):
return NotImplemented
return (self.x == other.x
and self.y == other.y)
a = Position(3, 4)
b = Position(3, 4)
c = a
print(a == b, a is b, a is c)
True False True
eq compares coordinates; is tests identity. Changing b’s coordinates can change equality while c remains an alias of a. Defining eq commonly makes an ordinary class unhashable unless hashing is deliberately supplied; any hash must agree with equality. This Position makes no dictionary-key contract.
Backpack length, indexing and membership
Backpack stores its own list and exposes selected operations:
class Backpack:
def __init__(self, items):
self._items = list(items)
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __contains__(self, item):
return item in self._items
bag = Backpack(["map", "apple"])
print(len(bag), bag[0])
print("apple" in bag)
2 map
True
len uses __len__, which must provide a non-negative integer; brackets use __getitem__; membership uses this class’s __contains__. Delegating to a list gives negative indexing and IndexError on an out-of-range index. The complete implementation also has add for Rover.pick.
Iterator design is deferred. A sequence-style fallback can request consecutive integer indices through getitem until IndexError when no iterator method is supplied. Slicing and other supported inputs require explicit contracts.
III. Organize the same project into modules
Begin with two real files
Save these files together and execute main.py. A module is an object with a namespace. Our own modules are Python source files; built-in and extension modules also exist.
rover.py
class Rover:
def symbol(self):
return "R"
def report(self):
return self.symbol()
class ScoutRover(Rover):
def symbol(self):
return "S"
main.py
from rover import ScoutRover
s = ScoutRover()
print(s.report())
python main.py
S
import rover binds the module object and leaves access through rover.ScoutRover. A from import binds the selected class reference directly in the importer. An alias changes the local access name. Explicit imports make origins clear; wildcard imports have export rules such as all and do not simply expose every possible name.
Why did importing a class start the demo?
Module initialization executes top-level statements, including an unguarded demo action:
rover.py
print("mission demo")
class Rover:
pass
main.py
import rover
print("main")
python main.py
mission demo
main
Guard the entry action with if __name__ == "__main__"::
rover.py
print("loading rover")
class Rover:
pass
if __name__ == "__main__":
print("mission demo")
main.py
import rover
import rover
print("main")
python main.py
loading rover
main
Direct execution of rover.py prints loading rover and mission demo. When main imports it, its name is rover and the demo branch is skipped. The unguarded loading message still runs. Executing class and def statements establishes objects; a function body waits for a call.
Trace first and repeated imports
The slides use Python3.13 line events from real files. main reaches import and waits while rover runs its top level, establishes Rover and skips the demo branch. main then receives the module binding. A second ordinary import under the same name in that process reuses sys.modules and does not repeat the loading message.
A module can enter sys.modules before all its definitions have been established. Presence in that cache alone does not mean initialization is complete. New processes, explicit reloads and cache manipulation need separate analysis.
A from import retains an object reference
This is the same binding-versus-mutation distinction used for L9’s shared backpack:
settings.py
count = 1
items = []
main.py
import settings
from settings import count, items
settings.count = 2
settings.items.append("apple")
print(count, settings.count, items)
python main.py
1 2 ['apple']
The local count still refers to 1 while settings.count is rebound to 2. Both items names retain the same list reference, making the apple visible. A from import neither copies the object nor establishes a live alias to a module field.
Full project layout and a package version
The third stage has this layout. File organization changes while role, position and backpack behavior retain the same contract.
03_expedition/
main.py
rover.py
geometry.py
backpack.py
Read the complete four-file project and its actual output
geometry.py
class Position:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __add__(self, other):
if not isinstance(other, Position):
return NotImplemented
x = self.x + other.x
y = self.y + other.y
return Position(x, y)
def __eq__(self, other):
if not isinstance(other, Position):
return NotImplemented
return (self.x == other.x
and self.y == other.y)
def __repr__(self):
return f"Position({self.x}, {self.y})"
backpack.py
class Backpack:
def __init__(self, items=()):
self._items = list(items)
def add(self, item):
self._items.append(item)
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __contains__(self, item):
return item in self._items
def __repr__(self):
return f"Backpack({self._items!r})"
rover.py
from geometry import Position
from backpack import Backpack
class Rover:
def __init__(self, name, x=0, y=0):
self.name = name
self.x, self.y = x, y
self._coins = 100
self.bag = Backpack()
def move(self, dx, dy):
self.x += dx
self.y += dy
def pick(self, item):
self.bag.add(item)
def spend(self, amount):
if type(amount) is not int or amount <= 0 or amount > self._coins:
return False
self._coins -= amount
return True
def coins_left(self):
return self._coins
def position(self):
return Position(self.x, self.y)
def symbol(self):
return "R"
def describe(self):
return f"{self.symbol()} {self.name} at ({self.x}, {self.y})"
def __str__(self):
return self.describe()
def __repr__(self):
return f"{type(self).__name__}({self.name!r}, {self.x}, {self.y})"
class ScoutRover(Rover):
def __init__(self, name, x=0, y=0, scan_range=2):
super().__init__(name, x, y)
self.scan_range = scan_range
def symbol(self):
return "S"
def describe(self):
return super().describe() + f", scan range {self.scan_range}"
class CargoRover(Rover):
def symbol(self):
return "C"
def describe(self):
return super().describe() + f", items {len(self.bag)}"
main.py
from rover import Rover, ScoutRover, CargoRover
from geometry import Position
def main():
team = [Rover("Ada"), ScoutRover("Lin", 3, 4), CargoRover("Sam", 5, 1)]
team[1].move(1, 0)
team[2].pick("apple")
for rover in team:
print(rover)
print(team[1].position() + Position(0, 1))
print(len(team[2].bag), "apple" in team[2].bag)
if __name__ == "__main__":
main()
python main.py
R Ada at (0, 0)
S Lin at (4, 4), scan range 2
C Sam at (5, 1), items 1
Position(4, 5)
1 True
The 04_package/expedition version includes init.py, main.py and the three modules. Its relative imports use forms such as from .rover import ScoutRover. Run python -m expedition from 04_package to establish the package context; its output matches the flat project. Regular and namespace packages are distinct mechanisms. A direct file launch can lack the required relative-import context.
IV. Inspect the runtime environment
Which module was actually imported?
This isolated directory deliberately includes a local random.py:
random.py
label = "local file"
main.py
import random
from pathlib import Path
print(Path(random.__file__).name)
print(random.randint(1, 3))
python main.py
random.py
AttributeError: module 'random' has no attribute 'randint' (consider renaming '<project>/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)
Inspect sys.executable, inspect a module’s __file__ when available, and check python -m pip --version. Compare these with VS Code’s selected interpreter. Startup mode, existing caches and import finders affect resolution; the current working directory is not universally first.
Keep project dependencies in an environment
python -m venv .venv
# macOS / Linux:
source .venv/bin/activate
# Windows PowerShell:
# .venv\Scripts\Activate.ps1
python -m pip --version
Select that environment’s Python and install only actual dependencies through python -m pip. This project needs no additional packages. Replace placeholders such as PACKAGE with a real dependency and check interpreter/platform compatibility. A venv isolates dependencies rather than providing a security sandbox. If PowerShell blocks activation, select or invoke the environment’s Python directly without changing a global policy for this lesson.
Module objects and compiled-code caches
sys.modules stores module objects within the current process. CPython can also cache compiled code in version-tagged pyc files under pycache. A valid disk cache can reduce recompilation, but a new process still initializes the module and executes its top level.
CPython compiles source to code objects/bytecode and executes it. Instruction details, cache tags and validity rules are implementation-specific. Permissions, launch options and changed source can affect caching. Bytecode is not source confidentiality, and raw pyc bytes are not source text for exec.
Review and transfer
For inherited calls, identify the receiver and the selected implementation. Initialization establishes state through assignments that actually execute. Operation protocols require a defined result, supported inputs and mutation behavior. Imports require a distinction between top-level execution, function calls, references and program entry.
The three classroom questions cover an explicit base-function call, equality versus identity, and imports with an entry guard. After b.x is changed, the equality question produces False, False, True. In a fresh process, the import question prints load and main. The slide examples and download provide the exact sources.
Lab10 practises inheritance and special methods through Shape, a Rover subclass, Money and Deck, then reuses shapes in a three-file report project. The starter supplies validation, money conversion and deck construction; a packing script prepares the module submission. Two short written cards record mechanisms and actual results. Complete the AI-use declaration once at the end of HW4. Solo assignment2 remains the five prime-counting methods; use the course calendar and submission page for its deadline. L11 studies work growth, exceptions and testing.
Optional reading
The extended mechanisms and additional applications remain available below. Choose an example for a particular question rather than reading them as another compulsory lecture.
Explore multiple inheritance, protocol boundaries, other classes and import details
Transfer to a Shape interface
The same inheritance and polymorphism mechanisms organize different area implementations. This example assumes valid dimensions; the lab separately specifies validation. An ordinary Shape can be instantiated and raises NotImplementedError when its area method is called.
import math
class Shape:
def area(self) -> float:
raise NotImplementedError("area")
class Circle(Shape):
def __init__(self, r: float) -> None:
self.r = r
def area(self) -> float:
return math.pi * self.r ** 2
class Square(Shape):
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side ** 2
class Rectangle(Shape):
def __init__(self, w: float, h: float) -> None:
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
shapes = [Circle(1), Square(2), Rectangle(2, 3)]
total = 0.0
for shape in shapes:
total += shape.area()
print(f"{total:.4f}")
13.1416
Augmented assignment and aliases
This implementation changes and returns self in iadd, while ordinary addition creates a new object. Other implementations may return a new value; missing or unsupported in-place methods can fall back to ordinary arithmetic.
class Value:
def __init__(self, n: int) -> None:
self.n = n
def __add__(self, other: "Value") -> "Value":
return Value(self.n + other.n)
def __iadd__(self, other: "Value") -> "Value":
self.n += other.n
return self
a = Value(1)
b = a
a += Value(2)
print(a.n, b.n, a is b)
a = a + Value(3)
print(a.n, b.n, a is b)
3 3 True
6 3 False
Person and Manager
class Person:
def __init__(self, name: str, job: str, pay: float) -> None:
self.name, self.job, self.pay = name, job, pay
def get_last_name(self) -> str:
return self.name.split()[-1]
def give_raise(self, fraction: float) -> None:
self.pay *= 1 + fraction
def __repr__(self) -> str:
return f"Person({self.name!r}, {self.job!r}, {self.pay:.2f})"
class Manager(Person):
def __init__(self, name: str, pay: float) -> None:
super().__init__(name, "manager", pay)
def give_raise(self, fraction: float, bonus: float = 0.1) -> None:
super().give_raise(fraction + bonus)
m = Manager("Alex Chen", 100)
m.give_raise(0.05)
print(m.get_last_name(), f"{m.pay:.2f}")
Person stores name, job and pay and provides surname lookup and a pay update. Manager overrides the update. Business constraints on fractions or bonuses would need a separate contract. Reaching a second same-named def in one namespace rebinds that name; it is not parameter-based overloading.
Cooperative inheritance and C3
class Root:
def visit(self):
return ["Root"]
class Left(Root):
def visit(self):
return ["Left"] + super().visit()
class Right(Root):
def visit(self):
return ["Right"] + super().visit()
class Combined(Left, Right):
def visit(self):
return ["Combined"] + super().visit()
print([c.__name__ for c in Combined.__mro__])
print(Combined().visit())
Super continues along this linearization, so Left can be followed by Right rather than its immediate base Root. Lookup order alone does not execute every method; the implementation must cooperate.
class A: pass
class B(A): pass
class C: pass
class D(B, C): pass
print([c.__name__ for c in D.__mro__])
D/B/A/C/object is not breadth-first search. Conflicting base order constraints can prevent class creation:
class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
Behavioral substitution also matters. A Rectangle interface independently changing width and height may not suit a Square invariant. Composition or delegation can be clearer.
Name mangling and subclass signatures
class Mapping:
def __init__(self, values):
self.items = []
self.__update(values)
def update(self, values):
self.items.extend(values)
__update = update
class Child(Mapping):
def update(self, keys, values):
self.items.extend(zip(keys, values))
m = Child([1, 2])
m.update(["a"], [3])
print(m.items)
The base’s private spelling reaches _Mapping__update, preserving its original function despite the child’s differently shaped update. Explicit external access remains possible; this is not a permission boundary.
Diagnostic inspection
class Animal: pass
class Dog(Animal): pass
d = Dog()
d.name = "Rex"
print(isinstance(d, Animal), type(d) is Animal)
print(issubclass(Dog, Animal))
print(d.__dict__)
print([c.__name__ for c in Dog.__mro__])
print([n for n in dir(d) if not n.startswith("_")])
Other useful attributes include instance.__class__, class.__name__, __bases__, and __mro__. Not every object has __dict__, and dir is not a complete supported-interface specification. Data descriptors are one exception to L09’s simple instance-field-first model; their implementation is outside this introductory example.
def add(a, b):
return a + b
print(add.__name__)
print(add.__code__.co_varnames)
print(add.__code__.co_names)
print(add.__code__.co_consts)
Functions can be named or passed as objects. co_varnames gives local names, co_names nonlocal references, and co_consts constants; a code object’s metadata is not an entire live execution state.
Protocol correspondence and limits
| Operations | Methods |
|---|---|
| <, <=, ==, !=, >, >= | __lt__, __le__, __eq__, __ne__, __gt__, __ge__ |
| +, -, *, abs | __add__, __sub__, __mul__, __abs__ and applicable reflected methods |
| += | __iadd__ and ordinary-addition fallback |
| len, [], []=, in | __len__, __getitem__, __setitem__, __contains__ |
| str, repr | __str__, __repr__ |
A right operand of a subtype can have reflected priority; same-type reflection is not automatically retried. Equality has no __req__ method. Two unsupported equality methods fall back to identity. Defining equality commonly makes a mutable class unhashable; a custom hash must agree with equality.
Implicit special operations generally consult the type rather than an ordinary instance field:
class Box: pass
def length():
return 3
b = Box()
b.__len__ = length
print(b.__len__())
print(len(b))
Calling the assigned method directly prints3, while len intentionally raises TypeError.
Geometry, complex numbers and isomorphic strings
class Triangle:
def __init__(self, a: float, b: float, c: float) -> None:
if min(a, b, c) <= 0 or not (a+b>c and a+c>b and b+c>a):
raise ValueError("invalid edges")
self.edges = [a, b, c]
def area(self) -> float:
a, b, c = self.edges
s = (a+b+c)/2
return (s*(s-a)*(s-b)*(s-c))**0.5
def __lt__(self, other: "Triangle") -> bool:
if not isinstance(other, Triangle):
return NotImplemented
return self.area() < other.area()
def __getitem__(self, index: int) -> float:
return self.edges[index]
def __setitem__(self, index: int, value: float) -> None:
candidate = self.edges.copy()
candidate[index] = value
Triangle(*candidate)
self.edges = candidate
a, b = Triangle(3, 4, 5), Triangle(3, 3, 3)
print(b < a, a[0])
a[0] = 4
print(a[0])
This contract orders triangles by area, indexes edges, and rejects an invalid replacement without altering old edges. It does not establish a universal meaning for adding or comparing triangles.
class ComplexNumber:
def __init__(self, real: float, imag: float) -> None:
self.real, self.imag = real, imag
def __add__(self, other: "ComplexNumber") -> "ComplexNumber":
if not isinstance(other, ComplexNumber):
return NotImplemented
return ComplexNumber(self.real + other.real, self.imag + other.imag)
def __repr__(self) -> str:
return f"ComplexNumber({self.real}, {self.imag})"
print(ComplexNumber(1, 2) + ComplexNumber(3, -1))
Point2D uses two coordinates; nDPoint can retain a scalar-coordinate list with an explicit dimension. L09’s complete Vector and HappyNumber examples remain available. The present two-dimensional operator example is distinct from the former n-dimensional method interface.
def isomorphic(s: str, t: str) -> bool:
if len(s) != len(t):
return False
forward, reverse = {}, {}
for a, b in zip(s, t):
if a in forward and forward[a] != b:
return False
if b in reverse and reverse[b] != a:
return False
forward[a], reverse[b] = b, a
return True
print(isomorphic("egg", "add"))
print(isomorphic("foo", "bar"))
The forward and reverse mappings enforce a one-to-one correspondence in both directions.
Reloading, circular imports and future statements
settings.py
print("load")
value = 1
main.py
import importlib
import settings
from settings import value
settings.value = 9
print(value, settings.value)
importlib.reload(settings)
print(value, settings.value)
Observed Python 3.13 output:
load
1 9
load
1 1
Use importlib.reload, not obsolete imp. Reload reruns initialization; prior from bindings are not automatically rebound. It is not a concurrent production hot-update guarantee.
a.py
import b
value = 1
b.py
from a import value
main.py
import a
Observed Python 3.13 output:
ImportError: cannot import name 'value' from 'a' (consider renaming '/privatea.py' if it has the same name as a library you intended to import)
A circular dependency can access an incompletely initialized name. Adjust responsibilities or defer the access rather than treating a different IDE as the semantic solution.
from __future__ import annotations
class Node:
def copy(self) -> Node:
return Node()
print(Node().copy().__class__.__name__)
A future statement is a compiler directive with placement rules allowing legal leading documentation/comments. It need not be the physical first line. Annotations has been optional since 3.7 and remains explicit in 3.13; it is not runtime type enforcement.
Decimal conversion for the lab
Lab10 supplies to_cents(); this optional section explains its rounding policy. Decimal constructs a decimal value from a string; quantize(Decimal(“1”)) rounds to integer units. ROUND_HALF_UP sends an exact half away from zero. Multiply into cents before that quantization, rather than truncating a binary floating-point product.
from decimal import Decimal, ROUND_HALF_UP
for yuan in [1.005, -1.005]:
value = Decimal(str(yuan)) * 100
cents = value.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
print(int(cents))
The results are 101 and −101 cents. This policy uses the supplied value’s str, not information lost in earlier float calculations. The lab supplies conversion through to_cents(); students focus on Money representation, arithmetic and comparison.
Package and distribution reference
Keep PYTHONPATH, site/.pth, site-packages and zip-based path knowledge. Sys.path entries are not all directories. __all__, regular/namespace packages, relative imports and -m describe different mechanisms.
NumPy provides arrays, broadcasting and numerical operations; Matplotlib draws figures; SciPy supplies scientific algorithms; pandas handles tables; pytest supports tests. Scrapy and Beautiful Soup support web work, while Pygame supports games. An ecosystem overview is not a requirement to install everything.
CPython, PyPy and Jython are implementations. Cython and Nuitka are compilation tools, not all interchangeable interpreters. Use the current CPython repository, not stale source-file line numbers.
Explicit compilation of trusted code
source = "answer = 2 + 3"
code = compile(source, "trusted.py", "exec")
namespace = {}
exec(code, namespace)
print(namespace["answer"])
Compile returns a code object; exec executes it in a chosen namespace. Only the authored, trusted source is used.
python -m py_compile module_a.py module_b.py
python -m compileall selected_project_directory
Select your teaching directory, not an entire machine. Compilation does not guarantee a writable cache or cross-version bytecode. Disassembly and cache inspection do not introduce a new security boundary.
Big integers: a later assignment
The calendar opens prime counting this week and big integers in week 12. Retain the original representation ideas: decimal-string input, separate sign and grouped digits, str/repr, eq/lt, and add/sub/mul. Converting a complete operand to Python int cannot replace the required arithmetic implementation, but int is a valid test oracle.
Consider zero, leading zeros, negatives, unequal lengths, negative differences, carries, borrows and long multiplication. A one-page report describes representation, signs and specific boundary/random comparisons. Use the later assignment specification for its exact interface and grading.