Runtime#
Python is more than a language; it’s a stack pick when they run their code. An implementation (CPython, PyPy, GraalPy, MicroPython), a memory model (reference counting plus a cyclic garbage collector), a bytecode the source compiles to, and an import system that loads, caches, and resolves modules at startup and on demand.
This page is the day-to-day reference for what happens between the operator’s source and the machine. For language semantics see Syntax and Types.
Memory model#
CPython is reference-counted. Every assignment, function
call, and container insertion increments a counter on the target
object; every binding’s death decrements it. When the counter
hits zero the object is freed immediately. A separate cyclic
garbage collector (gc module) sweeps reference cycles
periodically; otherwise refcounting alone misses them.
import sys, gc
sys.getrefcount(x) # how many references hold x (+1 for the call)
gc.collect() # force a cycle sweep
gc.get_count() # gen0 / gen1 / gen2 counters
gc.disable(); gc.enable() # control the cycle collector
The operator’s mental model. Objects with names live until the
last name is rebound or goes out of scope. Cycles
(a.ref = b; b.ref = a) live until the cycle collector runs.
__del__ is unreliable for cleanup; use __enter__ /
__exit__ or contextlib instead.
Object identity vs equality.
a = [1, 2]; b = a
a is b # True; same object
a == b # True; equal contents
a is [1, 2] # False; different object
id(a) # CPython: the object's address
Integers up to 256 and short strings are interned (one shared
object), so a is b may be True for small ints. Operator
never relies on this.
Implementations#
Python is a spec; “Python” usually means CPython, but the operator should recognise the alternatives.
Runtime |
Strategy |
Notes |
|---|---|---|
CPython |
bytecode interpreter |
The reference implementation. What every distro ships. Refcount + cycle GC. |
PyPy |
tracing JIT |
3-10× faster on long-running pure-Python loops. Slower startup; some C-extension friction. |
GraalPy |
GraalVM |
Interoperates with JVM languages; cross-language embedding. |
MicroPython |
bytecode interpreter for microcontrollers |
Subset of the language. ESP32, RP2040, embedded. |
CircuitPython |
fork of MicroPython |
Adafruit’s variant; same target class with extra hardware libraries. |
CPython interpreter flags worth knowing:
-O strips assert; -OO also strips docstrings;
-X dev enables development-mode checks;
-X faulthandler dumps a traceback on SIGSEGV.
Bytecode and dis#
Python source compiles to bytecode (.pyc files under
__pycache__/). The dis module shows what the interpreter
will execute.
>>> import dis
>>> dis.dis(lambda x: x + 1)
1 RESUME 0
LOAD_FAST 0 (x)
LOAD_CONST 1 (1)
BINARY_OP 0 (+)
RETURN_VALUE
.pyc files are cached in __pycache__/<name>.<tag>.pyc
keyed by interpreter version and source mtime. Stale cache plus
clock skew is the classic “I edited the file but the change
didn’t take” trap. python -B disables writing them;
PYTHONDONTWRITEBYTECODE=1 does the same.
Import system#
import evaluates a module top-to-bottom on first import and
caches the result in sys.modules. Subsequent imports return
the cached module without re-executing it. Circular imports
surface as ImportError or AttributeError on a half-
initialised module.
import sys
sys.path # where the interpreter searches
sys.modules.keys() # what is already loaded
import importlib
importlib.reload(my_module) # re-evaluate (REPL only; avoid in prod)
importlib.import_module("plugins.osint_collector") # by string
# find a module's source location
import mypkg
mypkg.__file__ # filesystem path
mypkg.__path__ # package search path
Search-path order, top to bottom: the directory of the script;
PYTHONPATH; the standard library; the installed
site-packages. The active virtualenv shadows the system
site-packages; sys.executable says which Python is
running.
For the package manager that installs into site-packages,
see Tooling.
Modules and packages#
Every Python file is a module. A directory of modules is a
package. The operator organises a project as a tree of packages
plus a few top-level scripts; the import system above walks
sys.path to find them and caches each one in sys.modules.
Module#
A module is a single .py file. Its top-level definitions
become attributes on the module object after import.
# mypkg/util.py
import re
_EMAIL = re.compile(r"^[\w.-]+@[\w.-]+$")
def is_email(s: str) -> bool:
return bool(_EMAIL.match(s))
def slugify(s: str) -> str:
return re.sub(r"[^\w]+", "-", s.lower()).strip("-")
After import mypkg.util as U, the operator reaches
U.slugify(...) and U.is_email(...). Names starting with
_ are conventional “private” but not enforced;
from X import * skips them unless the module declares
__all__.
Package#
A package is a directory of modules. Two flavours.
Regular package, directory with an
__init__.py(which can be empty). The init runs when the package is first imported.Namespace package (PEP 420), directory without
__init__.py. Multiple distributions can contribute sub-modules to the same namespace.
Standard layout for an operator’s CLI tool.
myproject/
├── pyproject.toml # build metadata + deps (PEP 621)
├── README.md
├── src/
│ └── mypkg/
│ ├── __init__.py # package init (often empty)
│ ├── __main__.py # `python -m mypkg` entrypoint
│ ├── cli.py
│ ├── collectors/
│ │ ├── __init__.py
│ │ ├── osint.py
│ │ └── shodan.py
│ └── util.py
└── tests/
└── test_cli.py
The src/ layout keeps the package out of the project root so
tests can’t accidentally import from the working directory and
bypass the installed copy.
Import forms#
import mypkg # bind name `mypkg`
import mypkg.util # bind `mypkg`, populate .util
import mypkg.util as U # alias
from mypkg.util import slugify # name binds directly
from mypkg.util import slugify as s # alias single name
from . import sibling # relative import (inside a package)
from ..parent_pkg import thing # relative, walk up
Style. Top of file, grouped: stdlib, third-party, local; each
group alphabetised, blank line between groups. ruff /
isort enforce this.
Entry points#
Two patterns for “what runs when type
python ...”.
The __name__ == "__main__" guard distinguishes import from
direct execution.
# mypkg/cli.py
def main():
...
if __name__ == "__main__":
main()
python -m mypkg runs mypkg/__main__.py which usually
delegates to a cli.main function. The preferred way to ship
a CLI in a package; works in any environment where the package
is importable.
Console scripts declared in pyproject.toml install as named
binaries.
[project.scripts]
myop = "mypkg.cli:main"
After pip install -e . (or uv sync), myop is on
PATH.
Circular imports#
The most common module bug. A imports B, B imports
A; whichever runs second sees a half-initialised module and
fails with ImportError or AttributeError.
Three working fixes, in order of preference.
Extract the shared names into a third module both import from. The cycle goes away.
Defer the import inside the function that needs it. The top-level import becomes a call-time import.
def encode(payload): from .crypto import sign # inside function, not at top return sign(payload)
Type-only imports for annotation-only uses. The
TYPE_CHECKINGconstant isFalseat runtime, so the import only fires for the type checker.from typing import TYPE_CHECKING if TYPE_CHECKING: from .other import OtherType def f(x: "OtherType") -> None: ...
Where packages land#
Standard library, inside the interpreter, no install step.
Third-party with venv,
<venv>/lib/python3.X/site-packages/.Third-party without venv, system
site-packages(root install) or usersite-packages(pip install --user,~/.local/lib/...).
$ python -c "import sys; print(sys.prefix, sys.exec_prefix)"
$ python -m site
For the install commands and resolver (pip, uv,
poetry), see Tooling.
GIL and free-threaded Python#
CPython traditionally runs one Python bytecode at a time per interpreter, regardless of how many OS threads exist. The Global Interpreter Lock (GIL) is the mutex that enforces this. Pure-Python CPU-bound work doesn’t scale across threads; I/O-bound work does (the GIL releases around blocking I/O).
Python 3.13 ships an optional free-threaded build (PEP 703)
that removes the GIL. Faster scaling for CPU-bound threaded
work; some C-extension friction; not the default in 3.13. Check
sys._is_gil_enabled() on 3.13+.
The operator’s working rules.
I/O-bound across cores → threads or asyncio (GIL or not, fine).
CPU-bound across cores → multiprocessing, free-threaded Python, or drop the hot loop into C / Rust / Cython / NumPy.
See Concurrency for the thread / process / asyncio deep dive.
References#
Syntax for the source the interpreter parses.
Types for the object model the runtime manages.
Tooling for the toolchain on top of the runtime (
pip,uv,ruff,mypy,pytest).Concurrency for threads, processes, asyncio, and the GIL.
Libraries for
sys,gc,dis,tracemalloc,importlib.PEP 328, relative imports.
PEP 420, namespace packages.
PEP 621, project metadata in
pyproject.toml.PEP 703, making the GIL optional.