Understanding how Python executes code, manages types, and handles memory makes every
other chapter easier to reason about. This chapter covers the key conceptual models that
distinguish Python from statically typed, compiled languages.
1.1 CPython and the Interpreter
The reference implementation of Python is CPython, written in C.
When you run a .py file the interpreter:
Parses source into an abstract syntax tree (AST).
Compiles the AST to bytecode (stored in .pyc files
inside __pycache__/).
Executes the bytecode on the CPython virtual machine.
Other implementations exist — PyPy (JIT-compiled, faster for long-running code),
Jython (JVM), MicroPython (embedded) — but CPython is the standard.
1.2 Dynamic Typing
Python is dynamically typed: types are associated with objects,
not with variable names. A variable is simply a reference (a label) that can point at
any object at any time.
x = 42 # x refers to an int object
x = "hello" # now x refers to a str object
x = [1, 2, 3] # now x refers to a list object
Type errors are caught at runtime, not at compile time. Optional
type hints (PEP 484) let you annotate types for static analysis
tools like mypy, but they have no effect at runtime.
1.3 Duck Typing
Python uses duck typing: if an object supports the required operations,
it works — regardless of its declared type.
"If it walks like a duck and quacks like a duck, it is a duck."
def total_length(items):
# works with any iterable whose elements have len()
return sum(len(item) for item in items)
total_length(["hello", "world"]) # 10
total_length(("ab", "cde")) # 5
Duck typing enables highly reusable code but shifts the responsibility for
type correctness to the programmer (or to a type checker like mypy).
1.4 Everything Is an Object
In Python every value is an object: integers, strings, functions, classes,
modules, and even None. Each object carries:
A type (accessed via type(obj))
An identity — unique id in memory (via id(obj))
A value
print(type(42)) # <class 'int'>
print(type(print)) # <class 'builtin_function_or_method'>
print(type(int)) # <class 'type'> — classes are objects too
1.5 The Global Interpreter Lock (GIL)
CPython uses a Global Interpreter Lock: a mutex that ensures only one
thread executes Python bytecode at a time. This simplifies memory management but limits
CPU-bound parallelism in multi-threaded code.
Practical implications:
I/O-bound tasks benefit from threading because threads release the GIL
while waiting for I/O.
CPU-bound tasks should use multiprocessing (separate processes, each
with their own GIL) or C extensions that release the GIL.
Python 3.13 introduces an experimental free-threaded build that removes
the GIL.
1.6 Memory Management
CPython manages memory with reference counting: each object stores
a count of how many references point at it. When the count reaches zero the object is
immediately deallocated.
Reference cycles (A → B → A) can prevent counts from reaching zero.
A supplemental cyclic garbage collector (the gc module)
detects and collects cycles periodically.
Small integer caching: CPython pre-allocates integers in
[-5, 256], so a is b may be True for small ints even when
created independently. Always use == for value equality.
1.7 Mutability
Python objects are either mutable or immutable:
Immutable
Mutable
int, float, bool, complex
list, dict, set
str, bytes, tuple
bytearray, user-defined classes (usually)
frozenset
Assignment never copies an object — it creates a new reference to the same object.
Mutable objects must be explicitly copied (list.copy(), copy.deepcopy())
when independent copies are needed.
1.8 Epilogue
This chapter described how Python runs code, why types live on objects rather than
variables, the trade-offs of the GIL, and the mutability model that governs data sharing.
The next chapter surveys the full breadth of the language.