CS1602Introduction to Computation
Lecture 10Part 4 Abstraction and OrganizationAI Level 1

Classes II and Modules

Reuse and override behavior, define Python operation protocols, and inspect real multi-file programs. Separate imported bindings from object mutation, then distinguish the interpreter environment, module caching, and compiled-code caching.

By the end of this lecture you should be able to

  • Identify the receiver and required state in inherited methods
  • Explain super as continued lookup along the method resolution order
  • Specify representation, arithmetic, comparison and container protocols
  • Distinguish module objects, imported bindings and shared references
  • Verify import effects and main guards using actual files
  • Separate interpreter configuration, process caches and disk bytecode caches
Contents

I. Inheritance and behavioral interfaces

Base classes and subclasses

L09 established state, receivers, and supported operations. Related classes can now reuse an implementation while supplying different behavior.

Inheritance connects a base to a subclass. A real-world classification is not sufficient justification: the subtype must meet the contract clients depend on.

class Animal:
    def __init__(self, name: str) -> None:
        self.name = name
    def speak(self) -> str:
        return "..."
    def introduce(self) -> str:
        return self.name + ": " + self.speak()

class Dog(Animal):
    def speak(self) -> str:
        return "woof"

class Cat(Animal):
    def speak(self) -> str:
        return "meow"

for animal in [Animal("A"), Dog("D"), Cat("C")]:
    print(animal.introduce())

Dog uses inherited initialization to establish its own name. The function implementing introduce comes from Animal, but self is the actual receiver and self.speak can select an override. Ordinary Python inheritance reaches object; this does not grant every object a length protocol.

Override without assuming initialized fields

Overriding replaces an inherited method implementation. A new initializer does not automatically execute the base initializer. This program intentionally lacks a field:

class Base:
    def __init__(self, value):
        self.value = value

class Child(Base):
    def __init__(self, value):
        self.extra = value

print(Child(3).value)

Finding a method through inheritance does not copy a base instance’s data. Coordinate initialization when the contract requires that state:

class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age
    def describe(self) -> str:
        return f"{self.name}, {self.age}"

class Student(Person):
    def __init__(self, name: str, age: int, school: str) -> None:
        super().__init__(name, age)
        self.school = school
    def describe(self) -> str:
        return super().describe() + ", " + self.school

s = Student("Alex", 18, "SJTU")
print(s.describe())

In this single-inheritance case super reaches Person. More generally it continues after the current class in the actual type’s MRO. It is not invariably the direct parent and does not automatically call every base method.

The order of initialization depends on state requirements, not a universal “first line” rule. Changes to signatures, results, exceptions, or invariants still require regression evidence.

Calls made inside inherited code

class Base:
    def word(self):
        return "base"
    def report(self):
        return self.word()

class Child(Base):
    def word(self):
        return "child"

print(Child().report())

The result is child. Base.report still makes its attribute access through the Child instance. Calling an inherited function does not replace self with a newly constructed base object.

Polymorphism and duck typing

One operation can select different implementations. Retain the complete common-base version first. It assumes valid dimensions for the example rather than providing all of the lab’s validation:

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}")

The observed total is 13.1416. A shared base documents this interface, but is not necessary for the compatible call below:

class Circle:
    def area(self) -> float:
        return 3.141592653589793
class Square:
    def area(self) -> float:
        return 4.0

total = 0.0
for shape in [Circle(), Square()]:
    total += shape.area()
print(f"{total:.4f}")

Both objects provide compatible no-argument area queries. Having the same method name alone does not establish compatible parameters, outcomes or side effects. A plain Shape method can raise NotImplementedError when called; that alone neither prevents instantiation nor establishes an abstract-class mechanism.

II. Python operation protocols

Display and diagnostic representations

Both methods return str. __str__ supplies readable output; __repr__ should make the object informative and preferably unambiguous. A reconstructible representation is a design goal, not a universal guarantee.

class Point:
    def __init__(self, x: float, y: float) -> None:
        self.x, self.y = x, y
    def __str__(self) -> str:
        return f"({self.x}, {self.y})"
    def __repr__(self) -> str:
        return f"Point({self.x}, {self.y})"

p = Point(3, 4)
print(p)
print(repr(p))
print([p])

The default str can fall back to repr, not vice versa. Containers display their elements’ repr. Do not treat a default address as a fixed output or feed an unknown representation to eval.

Arithmetic, reflection, and unsupported operands

NotImplemented is a sentinel returned to operator dispatch when this implementation does not support those operands. NotImplementedError is an exception. Neither means an equality result of False; do not test the sentinel’s truth value. In the example, math.hypot(x,y) returns the Euclidean norm of the two components.

import math

class Vector:
    def __init__(self, x: float, y: float) -> None:
        self.x, self.y = x, y
    def __repr__(self) -> str:
        return f"Vector({self.x}, {self.y})"
    def __add__(self, other: "Vector") -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)
    def __sub__(self, other: "Vector") -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x - other.x, self.y - other.y)
    def __mul__(self, scalar: float) -> "Vector":
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(self.x * scalar, self.y * scalar)
    def __rmul__(self, scalar: float) -> "Vector":
        return self * scalar
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self.x == other.x and self.y == other.y
    def __abs__(self) -> float:
        return math.hypot(self.x, self.y)

a=Vector(1,2)
b=Vector(3,4)
print(a+b, b-a)
print(a*3, 3*a)
print(abs(b), a==Vector(1,2))

Here addition/subtraction produce new vectors without changing inputs. Multiplication supports either scalar direction; abs queries the norm. Dispatch may involve the right operand, so a correspondence table is not a claim that the left method always runs alone. Reflected subtype priority and same-type restrictions are described later.

Unsupported equality can ultimately fall back to identity. Unsupported arithmetic or ordering generally raises TypeError. The numerical examples have moderate finite magnitudes, not a complete numeric-library contract.

Augmented assignment and aliases

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)

The first line is 3 3 True; ordinary addition then produces 6 3 False. This __iadd__ mutates and returns self; __add__ constructs another object. An in-place method may instead return a new object, and a missing method or sentinel can trigger ordinary dispatch. The += syntax does not guarantee preserved identity.

Length, indexing, assignment and membership

class Playlist:
    def __init__(self) -> None:
        self._songs: list[str] = []
    def add(self, song: str) -> None:
        self._songs.append(song)
    def __len__(self) -> int:
        return len(self._songs)
    def __getitem__(self, index: int) -> str:
        return self._songs[index]
    def __setitem__(self, index: int, value: str) -> None:
        self._songs[index] = value
    def __contains__(self, song: str) -> bool:
        return song in self._songs

p = Playlist()
p.add("first")
p.add("second")
p[1] = "changed"
print(len(p), p[-1], "first" in p)
for song in p:
    print(song)

Length must be a nonnegative integer. This implementation delegates indices and replacement to a list: negative indices work, out-of-range indices raise IndexError, and each instance owns its collection.

Without __iter__, iteration can fall back to requesting integer indices from zero until IndexError:

class Values:
    def __init__(self):
        self.items = ["A", "B"]
    def __getitem__(self, index):
        print("index", index)
        return self.items[index]

print(list(Values()))

It requests 0, 1, then 2 before ending. Explicit __iter__=None disables that fallback. L13 covers the full iterator protocol; no yield implementation is needed here.

Equality is not identity

class Item:
    def __init__(self, value):
        self.value = value
    def __eq__(self, other):
        if not isinstance(other, Item):
            return NotImplemented
        return self.value == other.value

a = Item(2)
b = Item(2)
c = a
print(a == b, a is b, a is c)

True False True reflects equal field values, distinct a/b objects, and the alias c. Identity tests can be intentional for any objects, not only None. Compare number/string values using equality rather than implementation-dependent interning. Use is None for the singleton convention without an unmeasured performance claim.

Exact type checks and isinstance’s subtype acceptance serve different contracts. Introspection helps diagnose objects but does not replace documentation of supported operations.

III. Modules, bindings and entry points

Actual files and module objects

Our own modules are .py files; Python also supports built-in, extension, and frozen modules. A module is an object with a namespace. Import binds it; from binds selected references in the importing namespace.

Save the following files together and run main.py under Python 3.13. A single browser source box is not a multi-file project and should not depend on earlier slides.

geometry.py

"""Geometry operations."""
def area(w, h):
    return w*h

report.py

"""Formatting operations."""
def line(value):
    return f"area={value}"

main.py

import geometry
import report
value = geometry.area(3, 4)
print(report.line(value))

Observed Python 3.13 output:

area=12

An import dependency does not mean every function in the imported module runs concurrently.

Explicit access paths

import math
import math as m
from math import sqrt
from math import sqrt as root
print(math.sqrt(16), m.sqrt(16), sqrt(16), root(16))

Aliases change access names locally, not the module’s own name. Keep standard-library, third-party, and local groups separate. Avoid wildcard imports in ordinary course code because bindings may be overwritten and origins obscured. Wildcards remain valid syntax with export rules, not an unconditional import of every name.

Imported references are not copies

config.py

count = 1
items = []

main.py

import config
from config import count, items
config.count = 2
config.items.append("x")
print(count, config.count, items)

Observed Python 3.13 output:

1 2 ['x']

The result is 1, 2, and the list containing x. Rebinding config.count does not rebind a previously imported count. The list is still shared, so its mutation is observed. Neither copying nor a live field link accurately describes from.

Initialization and the process cache

helper.py

print("module body")
def action():
    print("function body")

main.py

import sys
import helper
import helper
same = sys.modules["helper"]
print("cached", same is helper)
helper.action()

Observed Python 3.13 output:

module body
cached True
function body

Initial import executes top-level statements. Def creates a function rather than executing its body. Ordinary same-name imports in one process normally reuse sys.modules. A new process or an explicit reload has different conditions. Loaded-module counts and paths are environmental observations, not fixed answers.

Running directly or importing

tools.py

print("load")
if __name__ == "__main__":
    print("demo")

main.py

import tools
import tools
print("main")

Observed Python 3.13 output:

load
main

Tools has its module name during import, so demo is skipped while the unguarded load still runs. Executing tools.py directly enters its guarded block. A main guard only controls that block; it cannot remove other initialization effects.

A regular package

mypackage/init.py

"""A regular teaching package."""

mypackage/geometry.py

def area(w, h):
    return w*h

main.py

from mypackage import geometry
print(geometry.area(3, 4))

Observed Python 3.13 output:

12

Regular packages use __init__.py. Namespace packages are a separate mechanism, not a reason to omit the file indiscriminately. Relative imports need a package context, often supplied by python -m package.module; direct execution by a file path can fail.

IV. Search, environment and execution

Which module was selected?

Imports consult caches and finders; top-level path-based lookup uses sys.path, while package searches also involve package paths. A normal script commonly supplies its directory, but the working directory is not universally the first search location. Launch options, -I, environment variables, built-in/frozen modules and custom finders matter.

random.py

print("local random")

main.py

import random
print(random.randint(1, 3))

Observed Python 3.13 output:

local random
AttributeError: module 'random' has no attribute 'randint' (consider renaming '/privaterandom.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)

This controlled fresh script loads a local random file without randint, producing an intended AttributeError. Do not leave such files in a real project. Diagnose with sys.executable, __file__ when available, and python -m pip —version. An attribute error can also have other causes.

Environments and compatible distributions

python3 -m venv .venv
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell:
# .venv\Scripts\Activate.ps1
python -m pip --version
python -m pip install PACKAGE
python -m pip list
python -m pip install --upgrade PACKAGE
python -m pip uninstall PACKAGE
python -m pip install compatible_file.whl

PACKAGE and the wheel name are placeholders, not instructions to modify arbitrary software. A virtual environment isolates dependencies rather than providing a security sandbox. Wheel tags must match Python/ABI/platform; the old NumPy1.26.0 pin is unsuitable as a Python 3.13 teaching example. Install only necessary, compatible dependencies.

CPython compilation and execution

CPython is one Python implementation. Compilation can target native or intermediate forms, and execution can combine interpretation and JIT. “Compiler always means assembly” is not an accurate distinction.

import dis

def add(a, b):
    return a + b

dis.dis(add)

Recognize operand loading, addition, and return. Specific instructions and offsets are CPython-version details, not material to memorize.

Two different caches

sys.modules retains initialized module objects in the current process. A disk pyc retains compiled code. A new process may avoid recompilation while still executing initialization:

helper.py

print("helper")
value = 3

main.py

import helper
print(helper.value)

Observed Python 3.13 output:

Process 1:
helper
3
Process 2:
helper
3

Both fresh processes print helper and3. A directly run main script usually does not create its own cache, while an imported helper may. Cache validity, permissions, -B and the implementation affect what is written. Initial library costs can include dependencies and loading, not only compilation.

Bytecode is not a universal format for every Python implementation/version or a protection against reading the program. Exec does not directly treat a pyc binary as source text.

Reference: extended mechanisms and legacy examples

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}")

This retains name/job/pay, surname lookup, a pay update and a Manager override. 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

OperationsMethods
<, <=, ==, !=, >, >=__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"))

Two mappings enforce one-to-one correspondence. Retain the actual legacy task instead of merely listing its title.

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

Money needs a declared 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 still requires implementing Money; account interest uses the same quantization with its own units.

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. The old Coco name has no reliable identification here and remains an unverified historical mention. 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. The legacy20/20/20/10/15/5/10 rubric is historical, not a renewed current 7.5% project policy.

Course connection and practice

Lab10 specifies Shape classes, accounts, Money, Deck and real module organization. Solo assignment2 counts primes five ways, is worth 5%, and uses the shared due configuration. L11 develops algorithms, complexity, exceptions and tests. Measure a feasible workload for slow methods instead of substituting another algorithm.

Reuse and operator syntax need contracts for state, supported types, failure and effects. Imports need clear binding, initialization and environment conditions. Organization does not replace evidence.

Sources: Python 3.13 Classes, Data Model, Import.