Module 1: Everything Is an Object¶
Table of Contents¶
- Introduction
- Core 1: Functions as Objects
- Core 2: Classes as Objects
- Core 3: Modules as Objects
- Core 4: Instances as Objects
- Synthesis: The Core Cycle
- Capstone:
__code__-based Source Recovery Heuristic (Intentionally Bad Engineering) - Glossary (Module 1)
Introduction¶
Python treats functions, classes, modules, and instances as ordinary runtime objects rather than special syntax. You can store them in data structures, pass them as arguments, and even generate them dynamically. This is the foundation on which decorators, descriptors, metaclasses, plugin systems, and most “framework magic” are built.
This power is constrained by two forces: the Python Language Reference (PLR)—what is formally guaranteed—and the behaviour of real interpreters (primarily CPython). We target mainstream CPython 3.10+; behaviour on other interpreters or older versions may differ in details. Some aspects are portable and stable; others are implementation details that you must treat as off-limits in production code. Misjudging that boundary is the source of many subtle bugs and profiling/debugging surprises.
In this module we build a precise mental model of the object graph, organised into four foundational cores, plus a capstone that deliberately uses “bad engineering” to make the PLR/CPython boundary painfully concrete:
- Core 1: Functions as objects — functions as objects, with code objects, globals, and closures.
- Core 2: Classes as objects — classes as objects created by
type, exposing descriptors and method binding. (A descriptor is any object that defines__get__,__set__, or__delete__to customise attribute access; we return to this machinery in depth in Module 7.) - Core 3: Modules as objects — modules as objects cached in
sys.modules. - Core 4: Instances as objects — instances, how they store state (via
__dict__or__slots__), and the trade-offs of using__slots__to remove the per-instance__dict__. We introduce__slots__here and revisit it in later modules when we discuss dataclasses and descriptors.
The goal is to be able to explain, line by line, what happens when you write obj.attr or func() and how that ties back to modules and code objects.
When needed, we will distinguish:
- Spec-level behaviour – guaranteed (or at least intended) by the PLR and portable across implementations.
- CPython behaviour – true for mainstream CPython 3.10+ but not required by the specification.
- Diagnostic-only behaviour – surfaces CPython exposes for tooling and debugging (
__code__,__closure__, frame objects, etc.) that must never be the basis of core program logic.
In the rest of this text, anything that goes through inspect or documented attributes (__name__, __annotations__, etc.) counts as spec-level or supported introspection; anything that pokes directly at __code__, __closure__, or cell objects is treated as diagnostic-only CPython behaviour.
Core 1: Functions as Objects¶
Canonical Definition¶
In this core, function object refers specifically to a user-defined Python function—an instance of types.FunctionType. This distinguishes it from:
- built-in functions implemented in C (e.g.,
len,sum), and - other callables such as class instances that define
__call__.
All of these are callables in the sense that callable(obj) returns True, but only Python function objects expose the full set of introspection attributes described below.
A function object implements the callable protocol: func(arg) and func.__call__(arg) are equivalent. The same protocol applies to many other objects (classes with __call__, bound methods, etc.).
There are two common ways to ask “is this object callable?”:
-
callable(obj)— a runtime predicate: “does this object support being called?”. This is not a guarantee thatobj(...)will succeed for specific arguments; argument mismatch can still raiseTypeError. -
isinstance(obj, collections.abc.Callable)— checks via abstract base class machinery (and virtual subclass registration). It is not defined in terms ofcallable(), and the two can differ in edge cases.
The key attributes of a function object form a metadata API for introspection and limited modification:
__name__: unqualified name as a string (e.g.,"demo").__qualname__: qualified name incorporating nesting (e.g.,"outer.<locals>.inner").__doc__: docstring (strorNone, mutable).__module__: defining module as a string.__defaults__: tuple of defaults for positional-or-keyword parameters (mutable).__kwdefaults__: dict of defaults for keyword-only parameters (mutable).__annotations__: dict of type hints for parameters and return (mutable).__code__: code object describing compiled implementation details. Important precision: the code object itself is immutable, but on CPython you can rebindfunc.__code__to another code object (highly fragile; treat as diagnostic-only/experimentation).__globals__: live reference to the defining module’s global namespace (a dict).__closure__: CPython diagnostic surface — tuple of cell objects for closure variables; existence/order/content are implementation details.
Function object structure¶
Function Object Structure
┌──────────────────────────────────────────────────────────┐
│ Function object (types.FunctionType) │
├──────────────────────────────────────────────────────────┤
│ __code__ ──► code object (bytecode, varnames, etc.) │
│ __globals__ ──► defining module's namespace dict │
│ __closure__ ──► tuple of cell objects (free variables) │
│ __name__, __qualname__, __doc__, __annotations__, etc. │
└──────────────────────────────────────────────────────────┘
Caption: Code describes execution; globals/closure provide the environment.
Python vs built-in functions¶
The attributes listed above exist only for Python-defined functions. Built-in callables typically lack a __code__ object.
inspect.signature() support for built-ins is mixed: many built-ins expose signatures (often via __text_signature__), but some do not and may raise ValueError. Treat built-ins as “callable, but not uniformly introspectable”.
Key fields of __code__ (CPython diagnostic surface)¶
co_filename,co_firstlinenoco_varnames: argument names followed by local variable names.co_argcount: count of positional-or-keyword parameters (includes those with defaults; excludes positional-only, keyword-only,*args, and**kwargs).co_posonlyargcount: positional-only parameters (Python 3.8+).co_kwonlyargcount: keyword-only parameters.co_freevars: names this function reads from an enclosing scope.co_cellvars: local names this function provides as cells to nested functions.
Together, co_freevars and co_cellvars describe the closure relationship between nested functions.
In normal application logic, treat __code__ and its fields as diagnostic surfaces only. For portable behaviour, rely on inspect and documented attributes.
Deep Dive Explanation¶
Making functions first-class and uniformly callable enables generic infrastructure to accept “any callable” without caring about its concrete type. The metadata attributes expose just enough structure to power debuggers, serializers, and frameworks without requiring the original source code.
Some attributes are intentionally mutable (__defaults__, __kwdefaults__, __annotations__). Mutating them does not invalidate every cache in the ecosystem; tools that previously observed the function may retain stale snapshots indefinitely. Assume external tools may have cached your function’s metadata; never rely on mutations being noticed.
In CPython’s default pickle protocol, functions are typically reconstructed by re-importing the module named in __module__ and retrieving the function by its module-level name. Practical consequence:
Important limitation: only functions defined at module top level are reliably picklable by default. If __qualname__ contains "<locals>" (nested), there is no stable module-level name to recover, and default pickling fails.
Examples¶
Invocation equivalence:
def demo(x: int) -> str:
"""Doubles input as string."""
return f"Value: {x * 2}"
assert demo(3) == "Value: 6"
assert demo.__call__(3) == "Value: 6"
Mutation and caching pitfalls:
def api_call(version=1, debug=False):
return f"v{version}, debug={debug}"
import inspect
sig_old = inspect.signature(api_call)
print(sig_old.parameters["version"].default) # 1
api_call.__defaults__ = (2, True) # mutate defaults
sig_new = inspect.signature(api_call)
print(sig_new.parameters["version"].default) # 2
# Old Signature stays a stale snapshot.
print(sig_old.parameters["version"].default) # still 1
Globals are live:
CONFIG = {"debug": False}
def read_config():
return CONFIG["debug"]
read_config.__globals__["CONFIG"]["debug"] = True
print(read_config()) # True immediately
Closures Capture by Reference (Cells)¶
A closure is what lets an inner function continue to use variables from an outer function after the outer function has returned:
Here base is local to outer, but inner needs it after outer has finished. Python keeps base alive by placing it in a closure cell.
Closure Capture (Cells)
Outer function
│
└─► creates cell for captured variable (base)
│
▼
Inner function
│
└─► reads from cell (co_freevars includes "base")
Caption: Closures capture references via cells, not “snapshots of values”.
The code objects describe this relationship from both sides:
print("outer.co_freevars :", outer.__code__.co_freevars) # ()
print("outer.co_cellvars :", outer.__code__.co_cellvars) # ('base',)
print("inner.co_freevars :", add5.__code__.co_freevars) # ('base',)
print("inner.co_cellvars :", add5.__code__.co_cellvars) # ()
Interpretation:
outer.co_cellvars == ('base',)→outercreates a cell forbasebecause an inner function captures it.inner.co_freevars == ('base',)→innerreadsbasefrom a cell provided by its enclosing scope.
In CPython you can peek at the actual cells via add5.__closure__, but treat this as a diagnostic surface only.
Precision note: closures capture bindings (cells). If the captured object is mutable, mutations are observed. If the outer scope rebinds the name, whether the inner sees that rebinding depends on nonlocal and how the compiler arranged cells—this is exactly why cells are an implementation detail you do not build core logic on.
Advanced Notes and Pitfalls¶
- Lambdas, async functions, and generator functions are all plain
functionobjects until called. - Bound methods are
types.MethodTypewrappers around the original function (__func__) and the instance (__self__):
import types
class C:
def m(self):
return "ok"
c = C()
bm = c.m
assert isinstance(bm, types.MethodType)
assert bm.__func__ is C.m
assert bm.__self__ is c
assert bm() == "ok"
- Prefer the stable surface (
__name__,__qualname__,__module__,__annotations__,inspect.signature, etc.) for any application logic or serialization format. - Treat
__code__,__closure__, and cell contents strictly as diagnostic tools.
Exercise¶
Write a function that builds a readable signature string using only __code__, __defaults__, and __kwdefaults__ (handle positional-only and keyword-only correctly). Then mutate __defaults__ and show that a cached inspect.Signature object becomes permanently stale.
Core 2: Classes as Objects¶
At this point you only need a working intuition for descriptors: think of them as “smart attributes” on a class that participate actively in attribute access (@property is the most common example). The full descriptor protocol (and how functions themselves act as descriptors) is developed later, in Module 7.
Canonical Definition¶
A class is an instance of its metaclass (default type). Creation pipeline:
- Resolve metaclass (explicit
metaclass=, rightmost winning base, ortype). metaclass.__prepare__(name, bases, **kwds)→ namespace mapping (by default a plain dict that preserves insertion order on CPython 3.7+).- Execute class body in that namespace.
metaclass(name, bases, namespace_after_body, **kwds)creates the class object.- The class namespace is exposed via
cls.__dict__as a read-only mapping view (amappingproxyin CPython); attribute assignment still mutates the underlying namespace.
Key attributes:
__bases__: tuple of direct superclasses (mutable; changing it triggers MRO recomputation)__mro__: full method resolution order (C3 linearization)__class__: reference to the metaclass
Class creation pipeline (scan-first summary)¶
Class Creation Pipeline (PEP 3115)
Source: class C(Base1, Base2, metaclass=Meta): ...
Pipeline (definition time):
1. Resolve metaclass M
2. ns = M.__prepare__("C", (Base1, Base2), **kw) ← optional
3. Execute class body into ns
4. cls = M("C", (Base1, Base2), ns, **kw)
├─ M.__new__(...) ← create class object
└─ M.__init__(...) ← post-creation init
5. Bind cls to name "C" in scope
Caption: Metaclasses run before the class exists; this is definition-time work.
Class creation pipeline (expanded, step-by-step)¶
Expanded: Class Creation Pipeline (definition time)
┌──────────────────────────────────────────────────────────────┐
│ Step 1: Determine metaclass M │
│ • explicit metaclass= wins │
│ • else derived from bases (compatibility rules) │
│ • else default is built-in type │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Step 2: ns = M.__prepare__(name, bases, **kw) │
│ • must return a mapping │
│ • type.__prepare__ returns an empty dict │
│ • custom prepare may enforce order / collect declarations │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Step 3: Execute class body into ns │
│ • assignments populate ns │
│ • def statements create function objects with qualnames │
│ • no class object exists yet │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Step 4: cls = M(name, bases, ns, **kw) │
│ • M.__new__ creates class object │
│ • M.__init__ finalizes it │
│ • MRO computed and stored as cls.__mro__ │
│ • cls.__dict__ is a read-only view (mappingproxy on CPython)│
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Step 5: Bind cls into the surrounding scope │
│ • e.g. module globals: globals()["C"] = cls │
└──────────────────────────────────────────────────────────────┘
Caption: The class statement is executable code; it runs once at definition time.
Attribute lookup precedence (default object.__getattribute__)¶
Effective attribute lookup order for obj.attr in the default implementation:
- Data descriptors on the type or its bases (
__get__+ (__set__or__delete__)) - Instance storage (
obj.__dict__or slots) - Non-data descriptors (
__get__only) or plain attributes on the type/bases __getattr__fallback on the typeAttributeError
Attribute Lookup Precedence (obj.x)
1. Data descriptor in type(obj).__mro__? (has __set__/__delete__)
→ descriptor.__get__(obj, type(obj)) WINNER
2. Instance storage?
→ __dict__ (default) or slot field
3. Non-data descriptor or plain class attr in __mro__?
→ descriptor.__get__ (if present) or raw value
4. __getattr__ on type(obj)?
→ type(obj).__getattr__(obj, "x")
5. AttributeError
Caption: Data descriptors beat instance dict; non-data descriptors lose to it.
Examples: Data vs Non-Data Descriptor Precedence¶
class Data:
# Data descriptor: has __get__ and __set__
def __get__(self, obj, typ=None):
return obj._value
def __set__(self, obj, val):
obj._value = val * 2
class NonData:
# Non-data descriptor: only __get__
def __get__(self, obj, typ=None):
return 100
class C:
data = Data()
nondata = NonData()
c = C()
# Data descriptor always wins over instance __dict__
c.data = 10 # calls Data.__set__(c, 10) → c._value = 20
print(c.data) # calls Data.__get__(c, C) → 20
# Non-data descriptor can be shadowed by instance __dict__
c.__dict__["nondata"] = 999
print(c.nondata) # 999
Key point:
- Data descriptors override instance attributes.
- Non-data descriptors can be overridden by instance attributes.
Advanced Notes and Pitfalls¶
- Mutating
__bases__is a sharp tool. In normal CPython, you usually get an immediateTypeErrorif the new bases yield an inconsistent MRO or incompatible layout. Treatcls.__bases__ = ...as experiment-only. - Reassigning
__class__on instances is allowed only when the new class is layout-compatible with the old one; otherwiseTypeError. Treatobj.__class__ = NewTypeas a controlled experiment, not a production technique. - The descriptor precedence rules are not a curiosity; they are the mechanism behind
@property, method binding, many ORMs, and framework-level attribute tricks.
Exercise¶
Two-tier exercise (same goal; pick one):
-
Baseline (no metaclass yet): implement “no redeclare” checking using
__init_subclass__to inspectcls.__dict__after creation. -
Metaclass version (PEP 3115): create a metaclass whose
__prepare__returns a dict-like mapping that raises if a name is assigned twice in the class body. Then trycls.__bases__ = ...to force an MRO error and observe theTypeError.
Core 3: Modules as Objects¶
Canonical Definition¶
A module is an instance of types.ModuleType, cached in sys.modules. There is exactly one module object per imported module name; every import returns another reference to the same object.
Import pipeline (PEP 451):
- Finder →
ModuleSpec - Loader creates module object (default
ModuleType(name)) exec_module(module)runs the code intomodule.__dict__- Cache in
sys.modules
importlib.reload(mod) re-executes the module code into the existing module object; it does not replace the object itself.
When a file is executed as a script, its module name is "__main__"; imports see it under its real package name instead.
Modules may define __all__ to control what from module import * exports; it has no effect on direct attribute access.
Examples¶
One module object, many names (runnable)¶
import sys
import types
import importlib
name = "example_synthetic"
m = types.ModuleType(name)
m.value = 123
sys.modules[name] = m
m2 = importlib.import_module(name)
print(m is m2) # True
print(m2.value) # 123
Reload and stale references (runnable “reload simulation”)¶
import sys
import types
mod = types.ModuleType("mod_synthetic")
sys.modules[mod.__name__] = mod
exec('value = "old"', mod.__dict__)
imported_value = mod.value # simulates: from mod import value
print(mod.value, imported_value) # old old
# Simulate reload: re-exec into the same module object.
exec('value = "new"', mod.__dict__)
print(mod.value) # new
print(imported_value) # old (stale)
Advanced Notes and Pitfalls¶
from module import namecreates a separate name binding that reload never updates.- Prefer
import mod; mod.valueoverfrom mod import valuein reload-heavy workflows.
Exercise¶
Implement a proxy object that wraps a module and refreshes attributes on each access (useful in REPL/reload-heavy workflows). Demonstrate the staleness bug with plain from ... import.
Core 4: Instances as Objects¶
Canonical Definition¶
Instances are created by cls.__new__ and then initialized by cls.__init__.
By default, each instance has its own attribute dictionary:
So inst.x = 1 is, in the default model:
Declaring __slots__ on a class changes this storage model:
- Python allocates a fixed layout for the names in
__slots__. - For that class, Python does not create a per-instance
__dict__automatically (unless'__dict__'is explicitly listed in__slots__or inherited from a base class). - Attributes named in
__slots__live in slot storage instead of a dict.
So:
- Without
__slots__– dynamic and tool-friendly, but heavier. - With
__slots__– fixed and lean, but less flexible and can break generic tooling.
Slots are a targeted optimization. Use them when you have measured that memory footprint or attribute access is a real bottleneck and you have many instances with a stable shape.
Instance layout: __dict__ vs __slots__¶
Instance Layout: __dict__ vs __slots__
Dictionary-backed (default)
┌────────────────────────────────────┐
│ Instance │
├────────────────────────────────────┤
│ __class__ ──► Class │
│ __dict__ ──► {x: 10, y: 20, ...} │
└────────────────────────────────────┘
Slotted (fixed layout)
┌────────────────────────────────────┐
│ Instance │
├────────────────────────────────────┤
│ __class__ ──► Class │
│ x = 10 (slot) │
│ y = 20 (slot) │
│ (no __dict__ unless requested) │
└────────────────────────────────────┘
Caption: Slots trade flexibility for memory/speed; no dynamic attrs by default.
Attribute lookup pipeline (read access)¶
This is the same precedence used in Core 2; the difference here is that “instance storage” may be a dict or slot fields.
Attribute Lookup Precedence (obj.x) — storage detail
1. Data descriptor in type(obj).__mro__?
2. Instance storage:
• dict-backed: obj.__dict__
• slotted: slot field backing store
3. Non-data descriptor / class attribute in MRO
4. __getattr__ on type(obj)
5. AttributeError
Caption: Descriptors decide semantics; storage decides where instance state lives.
Examples¶
Memory and behaviour¶
Memory difference is platform/version dependent; the code prints measured sizes on your runtime.
import sys
class Regular:
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
class Slotted:
__slots__ = ("a", "b", "c")
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
r = Regular(1, 2, 3)
s = Slotted(1, 2, 3)
# sys.getsizeof excludes referenced objects; use it for relative comparisons.
print("Regular instance:", sys.getsizeof(r), "dict:", sys.getsizeof(r.__dict__))
print("Slotted instance:", sys.getsizeof(s))
Behavioural difference (no dynamic attributes when only slots are present):
class Point:
__slots__ = ("x", "y")
p = Point()
p.x = 1
p.y = 2
try:
p.z = 3
except AttributeError as e:
print("Expected:", e)
If you need both fixed slots and dynamic attributes, explicitly reintroduce __dict__:
class Hybrid:
__slots__ = ("x", "y", "__dict__")
h = Hybrid()
h.x = 1 # slot
h.extra = "ok" # dict
print(h.x, h.extra)
Advanced Notes and Pitfalls¶
- Many generic tools assume instances have a
__dict__. Pure slotted classes can break these silently unless the tool explicitly supports slots. - Pickling slotted instances works, but default pickle records slot values; tools that walk
__dict__may require custom state hooks. - If you need weak references, include
'__weakref__'in__slots__. - If any base class has a
__dict__, subclasses will effectively have a dict as well; slots then only cover additional fixed fields.
Exercise¶
Create a deep inheritance chain of slotted classes and measure memory per instance at each level. Then introduce a base class without __slots__ and observe that all subclasses regain a full __dict__, even if they still declare __slots__.
Synthesis: The Core Cycle¶
class Processor:
def process(self, data):
return f"Processed {len(data)}"
import sys
mod = sys.modules[__name__]
cls = mod.Processor
func = cls.process
inst = cls()
bound = inst.process
assert bound.__func__ is func
assert func.__globals__ is mod.__dict__
assert inst.__class__ is cls
print(bound("abc")) # Processed 3
The Core Object Cycle
Module object
│
▼
Class object (from module.__dict__)
│
▼
Instance (Class())
│
▼
Bound method (inst.method)
│
├─► __func__ ──► Function object
│ │
│ └─► __globals__ ──► Module.__dict__
└─► __self__ ──► Instance
Caption: Everything loops back: module → class → instance → method → function → module.
Key mental model:
- Module holds names (globals dict).
- Class is an object stored in that dict.
- Instance is created by the class.
- Bound method binds
(function, instance). - Function executes using module globals and (optionally) closure cells.
Capstone: __code__-based Source Recovery Heuristic (Intentionally Bad Engineering)¶
This capstone is intentionally bad engineering. The goal is to show why __code__ is a diagnostic surface and why “recover source from __code__” is fundamentally brittle.
Canonical Idea¶
Given a function object f, you can:
- read
f.__code__.co_filenameandf.__code__.co_firstlineno, - open that file,
- slice lines that “look like” the function definition/body based on indentation.
On tiny scripts, this appears to work. On anything realistic, it fails in subtle and misleading ways.
Why This Is Inherently Fragile¶
The __code__ object exposes:
co_filename: where the code thinks it comes from (can be real, temp, or pseudo like"<ipython-input-...>")co_firstlineno: approximate starting line (often thedefline; not a formal contract)- no stable end-line boundary.
The typical heuristic (“scan until indentation drops”) breaks for:
- decorated functions (you retrieve wrapper, not the original),
- multiline signatures and formatting tricks,
- nested functions (context lost),
- non-file environments (REPL/Jupyter/zipapps/frozen binaries).
Example: A Fragile print_source¶
This implementation is intentionally not robust; it is a teaching tool.
def print_source(func):
"""
Educational demo only.
"""
code = func.__code__
filename = code.co_filename
start_line = code.co_firstlineno - 1
if not filename or "<" in filename:
raise OSError(f"Cannot read real file for {filename!r}")
with open(filename, "r", encoding="utf-8") as f:
lines = f.readlines()
if start_line < 0 or start_line >= len(lines):
raise ValueError("co_firstlineno is out of bounds for file")
def_line = lines[start_line]
def_indent = len(def_line) - len(def_line.lstrip(" \t"))
result = [def_line]
i = start_line + 1
while i < len(lines):
line = lines[i]
stripped = line.lstrip(" \t")
cur_indent = len(line) - len(stripped)
if stripped and cur_indent <= def_indent and not stripped.startswith(")"):
break
result.append(line)
i += 1
while result and not result[-1].strip():
result.pop()
print("".join(result), end="")
One harness, two behaviours: inspect.getsource vs print_source¶
This demo is intentionally written so it behaves sensibly both in “real file” runs and in REPL/Jupyter contexts (where failures are expected).
import inspect
def demo(x, y=1):
"""Simple demo function for print_source."""
if x > 0:
return x + y
return x - y
def decorated(f):
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
@decorated
def decorated_demo(x, y):
return x * y
def multi_line_signature(
x,
y,
z=1,
):
return x + y + z
def outer():
def inner(t):
return t + 1
return inner
targets = [demo, decorated_demo, multi_line_signature, outer()]
for fn in targets:
print(f"\n=== inspect.getsource({fn.__qualname__}) ===")
try:
print(inspect.getsource(fn).rstrip())
except OSError as e:
print("Expected (non-file context):", e)
for fn in targets:
print(f"\n=== print_source({fn.__qualname__}) ===")
try:
print_source(fn)
print()
except (OSError, ValueError) as e:
print("Expected:", e)
What to observe (when run from a real .py file):
demomay look “correct”.decorated_demooften returns wrapper code (wrong target).multi_line_signaturesometimes survives; small formatting changes break it.- the nested
inneris context-free.
Exercise¶
-
Run
print_sourceon: -
decorated functions,
- nested functions,
- functions defined in REPL/Jupyter,
-
code packaged as a zipapp or run by tooling that rewrites filenames.
-
Rewrite
print_sourceto first tryinspect.getsource(func)and raise a clear error if that fails. Compare behaviour across the same cases.
After you do this once, you should be permanently skeptical of any tool that claims to “reconstruct source from __code__” as a core capability.
You have completed Module 1.
Glossary (Module 1)¶
| Term | Definition |
|---|---|
| Everything is an object | Python functions, classes, modules, and instances are runtime objects that can be stored, passed, inspected, and generated dynamically. |
| Object graph | The runtime network linking module → class → instance → bound method → function → module globals (the “core cycle”). |
| Spec-level behavior (PLR) | Semantics intended/guaranteed by the Python Language Reference; portable across implementations. |
| CPython behavior | Behavior specific to the CPython interpreter (common and stable in practice, but not always guaranteed by the spec). |
| Diagnostic-only surface | Introspection hooks exposed for debugging/tooling (e.g., __code__, __closure__, frames); should not be core logic dependencies. |
| Function object | A user-defined Python function (types.FunctionType), distinct from built-ins and other callables. |
| Built-in function | A callable implemented in C (e.g., len), often lacking __code__; signature support may be incomplete. |
| Callable | Any object for which callable(obj) is true; includes functions, bound methods, classes, and instances implementing __call__. |
| Callable protocol | Rule that obj(...) invokes the object’s call mechanism (__call__ for many user objects; interpreter slots for built-ins). |
| Function metadata | Introspection/mutation surface like __name__, __qualname__, __doc__, __module__, __defaults__, __kwdefaults__, __annotations__. |
__defaults__ / __kwdefaults__ |
Mutable default argument storage for positional-or-keyword and keyword-only params; mutating can desync caches (e.g., old inspect.Signature). |
| Stale introspection snapshot | Cached metadata (e.g., an inspect.Signature) that does not update after you mutate function attributes. |
| Code object | Immutable compiled representation (func.__code__) containing bytecode and compilation metadata (names, constants, filename, line numbers). |
Rebinding __code__ |
CPython allows swapping a function’s __code__ with another code object; extremely fragile and treated as diagnostic/experimentation. |
__globals__ |
Live reference to the defining module’s global namespace dict; changes are immediately visible to the function. |
| Closure | A function plus captured free variables from an enclosing scope, kept alive via cells after the outer function returns. |
| Closure cell | CPython object holding a captured binding; inner functions read captured values through these cells (visible via __closure__). |
| Capture by reference | Closures capture bindings (cells), not value snapshots; mutations of captured objects are observed. |
| Picklability limitation | Default pickling typically reconstructs functions by module + top-level name; nested functions ("<locals>") generally fail. |
| Bound method | The object returned by inst.method: pairs __func__ (function) with __self__ (instance) and auto-supplies self. |
| Descriptor | Any object defining __get__, __set__, or __delete__ to participate in attribute access; foundation of @property, methods, and frameworks. |
| Data descriptor | Descriptor with __set__ and/or __delete__; overrides instance storage during lookup (cannot be shadowed by instance attributes). |
| Non-data descriptor | Descriptor with only __get__; can be shadowed by an instance attribute of the same name (plain methods behave this way). |
| Attribute lookup precedence | Default order for obj.attr: data descriptor → instance storage (__dict__/slots) → non-data descriptor/class attr → __getattr__ → error. |
| Class object | An object representing a type; typically an instance of metaclass type, created at definition time via the class-creation pipeline. |
| Metaclass | The “class of a class” (default type); controls class creation (__prepare__, __new__, __init__). |
| MRO | Method Resolution Order (cls.__mro__), the C3-linearized search order for attributes across base classes. |
mappingproxy |
Read-only view of a class’s dict (cls.__dict__) in CPython; reflects live namespace but cannot be mutated directly. |
| Module object | Instance of types.ModuleType, holding a namespace dict and cached by name in sys.modules. |
sys.modules cache |
Global module cache ensuring imports return the same module object per name; foundational for import identity. |
| Reload re-executes | importlib.reload(mod) re-runs code into the same module object; existing external references (e.g., from mod import x) stay stale. |
| Import-time side effect | Work performed at definition/import time (registration, I/O, heavy computation); scales poorly and creates subtle order bugs. |
| Instance | Object created by calling a class (cls()), with state stored in __dict__ by default or in slots when using __slots__. |
__dict__-backed storage |
Default per-instance mutable mapping for attributes; flexible and tooling-friendly but memory-heavier. |
__slots__ |
Declares a fixed per-instance layout for named attributes; reduces memory and restricts dynamic attribute creation unless __dict__ is included. |
| Hybrid slots | A slotted class that includes "__dict__" (and optionally "__weakref__") to regain dynamic attributes while keeping fixed slots. |
| Layout compatibility | Constraint for reassigning obj.__class__ or changing cls.__bases__; requires compatible memory layout/MRO, otherwise TypeError. |
| Introspection boundary | Practical rule: use stable APIs (inspect, documented attributes) for logic; treat __code__/cells/frame internals as diagnostic only. |
| Source recovery heuristic | Attempting to reconstruct source from co_filename/co_firstlineno + indentation scanning; brittle and misleading in real systems. |
| Decorator wrapper pitfall | Source/introspection often targets the wrapper function, not the original, unless metadata is preserved (e.g., via functools.wraps). |
| ORM (Object–Relational Mapper) | Framework mapping Python attributes to database operations; typically leverages descriptors and metaclasses. |
Module 2 will cover safe introspection tools (dir, vars, getattr, inspect) and how to use them without falling into the implementation-detail traps we have just mapped.