This document scopes one route to the Python 3.15 standard library: copy
CPython's Lib/ tree unchanged and have Small Python supply what that tree
expects from the interpreter underneath it. It is a scope, not a status
report. Nothing listed here is implemented unless the manual
says so, and per the project's rules no library module is added without an
explicit request.
CPython's standard library has two halves:
Lib/holds about 600 Python modules (160 public top-level modules and packages, excludingtest,idlelibandturtledemo). These are plain Python 3.15 source. Small Python can run them as they are.Modules/,Python/andObjects/hold the native modules thatLib/imports (_io,_sre,_thread,posix,math, ...). They are written against CPython's C API and internal headers, so they cannot be copied. Small Python must provide each one again, under the same name and with the same interface, written on its own C API.
So the plan is:
- Copy
Lib/byte for byte from the pinned tag (v3.15.0, the oracle undertests/upstream/cpython). Never patch a file in it. Updating to a later 3.15 release means copying the tree again. - Provide the native modules that
Lib/imports. They live at the host boundary, likespy-std.c, and stay out oflibspy.a. The core keeps onlybuiltins. - Close the engine and host gaps that
Lib/reaches without importing a module: the import system,sys, code and frame objects (§3).
Small Python keeps its own sys and gc (modules/). In CPython these are
built in, not part of Lib/, so nothing gets copied over them.
Every module under the pinned Lib/ was parsed with the 3.15 oracle's ast.
An import counts as required when it runs at module level and is not
guarded by except ImportError. An import counts as optional when it is
guarded, sits inside a function, or sits under a platform test. Imports were
then followed through Lib/ to the native modules they end at. Lazy imports
(lazy import) count as required, because the first use loads them.
The optional imports were then checked by hand. Many have a pure-Python
fallback (_pydatetime, _pydecimal, _py_abc, _py_warnings, Python code
in heapq, bisect, json, pickle, operator, functools). Others have
none, and in those cases a missing module means missing behaviour:
collections.dequeanddefaultdictexist only in_collections.hashlibwithout_md5,_sha1,_sha2,_sha3or_blake2provides no hashes.subprocesson POSIX needs_posixsubprocess.
The tables below include those corrections. Windows-only modules (nt,
_winapi, msvcrt, winreg, winsound, _overlapped, _wmi) are listed
separately; the host targets POSIX first.
Lib/ relies on the interpreter in ways that no native module covers.
Import system. Lib/ needs these things from the importer:
- a
sys.pathsearch over a directory tree; - packages with
__path__, plus__spec__,__loader__and__file__; sys.meta_pathandsys.path_hooks.
The spy command's loader currently resolves only paths next to the script
(manual §3.2). The route that fits "unchanged" best is the one CPython uses:
run Lib/importlib/_bootstrap.py and _bootstrap_external.py as the import
machinery, installed through _imp, and expose them as _frozen_importlib
and _frozen_importlib_external. sys.implementation.cache_tag = None
already turns off .pyc writing.
sys surface. The table below counts how often public Lib/ modules
use names that Small Python's sys does not have yet. The names in place
include path, meta_path, flags, _getframe, _getframemodulename and
the hooks.
| name | uses in Lib/ |
used by |
|---|---|---|
sys.audit |
55 | os, socket, subprocess, urllib, shutil, tempfile, ... |
sys.monitoring |
22 | bdb, pdb, profiling |
sys._stdlib_dir |
2 | sysconfig, site |
Code, frame and traceback objects. inspect, traceback, dis, pdb
and doctest read co_positions(), co_lines(), co_code, f_lasti and
tb_lasti. Small Python executes AST nodes, not CPython bytecode. Positions
and line tables can be real. co_code, _opcode and dis output describe
CPython's bytecode, so dis can import but cannot disassemble meaningfully.
compile and ast. ast.parse calls compile(..., PyCF_ONLY_AST), and
_ast exposes the node classes. Both must expose the engine's own parse
tree, in CPython's node shapes.
Language features. Lib/ 3.15 uses lazy imports, frozendict and
template strings throughout. All three are in the core already.
traceback, os, re, io, threading, inspect, typing, warnings
and importlib each pull in most of this set, so almost every real program
needs all of it:
| module | role |
|---|---|
_io |
file, buffer, and text I/O classes behind io and open (_pyio imports io, so it is no fallback) |
posix |
os functions on POSIX; os is required by 94 public modules |
errno |
error-number constants |
time |
clocks, struct_time, strftime |
_thread |
locks, RLock, start_new_thread, get_ident |
_weakref |
ref, proxy, getweakrefcount; Small Python's weak table backs it |
_contextvars |
ContextVar, Context, Token |
_collections |
deque, defaultdict, OrderedDict, _tuplegetter, _count_elements |
itertools |
all iterator tools |
_operator |
operator accelerator; hmac imports _compare_digest from it directly |
_sre |
regular-expression engine under re |
_codecs |
codec registry, built-in codecs |
_warnings |
warnings filters (_py_warnings is a fallback) |
_imp |
import machinery hooks for importlib._bootstrap |
marshal |
serialization imported by importlib._bootstrap_external and pkgutil |
_ast |
AST node classes for ast, inspect, dataclasses, annotationlib |
_tokenize |
TokenizerIter for tokenize, linecache, inspect |
_typing |
TypeVar, ParamSpec, Generic support for typing |
_opcode |
bytecode metadata imported by dis, which inspect imports |
These need only the C library. Each one makes the public modules in the last column work.
| native | public modules it unlocks |
|---|---|
math |
datetime, decimal, fractions, random, statistics, tomllib, calendar, ... (37) |
_struct |
struct, pickle, zipfile, tarfile, gzip, wave, shelve (28) |
binascii |
base64, email, zipfile, ssl, secrets (21) |
_random |
random, tempfile, secrets, statistics, email (17) |
_string |
string, logging, email (17) |
_signal |
signal, subprocess, asyncio, unittest (15) |
select |
selectors, subprocess, asyncio, socketserver, pty (12) |
atexit |
logging, asyncio, unittest, multiprocessing (9) |
_socket |
socket, http, urllib, asyncio, smtplib, ftplib (21) |
_posixsubprocess, fcntl |
subprocess |
pwd, grp |
pathlib, shutil, tarfile, getpass (optional) |
termios |
tty, pty, getpass |
_stat |
stat accelerator (fallback in stat.py) |
_md5, _sha1, _sha2, _sha3, _blake2, _hmac |
hashlib, hmac, and random seeding from str/bytes; CPython vendors portable HACL* C for these |
unicodedata |
stringprep, encodings.idna, re (optional), traceback (optional); built from Small Python's generated Unicode tables |
_csv |
csv |
_symtable |
symtable |
_locale |
locale (Python emulation otherwise) |
_multibytecodec, _codecs_cn, _codecs_hk, _codecs_iso2022, _codecs_jp, _codecs_kr, _codecs_tw |
CJK codecs in encodings |
array, mmap, cmath |
the modules of the same name |
_lsprof |
cProfile |
_tracemalloc, faulthandler |
the modules of the same name; need allocator and signal hooks in the engine |
Lib/ already has a Python fallback for each of these, so skipping them is
correct, only slower:
| module | fallback |
|---|---|
_abc |
_py_abc |
_bisect, _heapq |
Python code in bisect, heapq |
_datetime |
_pydatetime |
_decimal |
_pydecimal |
_functools |
Python code in functools |
_json |
Python scanner in json |
_pickle |
Python code in pickle |
_elementtree |
Python code in xml.etree.ElementTree |
_statistics, _queue, _zoneinfo, _types, _uuid, _asyncio, _sysconfig, _suggestions |
Python code in the owning module |
CPython builds these against third-party libraries. Small Python allows no
runtime dependency beyond the C library and libm (AGENTS.md). Each module
below must either vendor its library as C source, as CPython does for expat
and libmpdec, or stay an opt-in build:
| module | library | public modules |
|---|---|---|
zlib |
zlib | gzip, zipfile (deflate), compression |
pyexpat |
expat (CPython vendors it) | xml, plistlib, xmlrpc |
_bz2 |
libbz2 | bz2 |
_lzma |
liblzma | lzma |
_zstd |
libzstd | compression.zstd |
_ssl, _hashlib |
OpenSSL | ssl; faster hashlib |
_sqlite3 |
SQLite | sqlite3, dbm.sqlite3 |
_ctypes |
libffi | ctypes |
_curses, _curses_panel |
ncurses | curses |
readline |
readline/libedit | interactive line editing (optional everywhere) |
_dbm, _gdbm |
ndbm, gdbm | dbm.ndbm, dbm.gnu |
_tkinter |
Tcl/Tk | tkinter, turtle |
-
Windows:
nt,_winapi,msvcrt,winreg,winsound,_overlapped,_wmi. -
macOS proxy settings:
_scproxy.urllib.requesttreats it as optional. -
CPython runtime internals:
_interpreters,_interpqueuesand_interpchannels(subinterpreters;concurrent.interpreters);_remote_debugging;sys.monitoringevents at bytecode granularity.
concurrent.futuresimports_interpretersunderexcept ImportErrorand simply omitsInterpreterPoolExecutor, so this exclusion blocks onlyconcurrent.interpreters._remote_debuggingis used only byasyncio.toolsandprofiling.sampling. -
Process-based parallelism:
_multiprocessingand_posixshmem.multiprocessingspawnssys.executableand pickles across processes, so it needs aspyexecutable that can stand in forpython. -
CPython's own tests and examples:
_testcapi,_testinternalcapi,_testbuffer,_testclinic*,_testmultiphase,_testsinglephase,_testimportmultiple,_xxtestfuzz,_ctypes_test,xxlimited*andxxsubtype.
Sixteen public modules need no native module at all: bisect cmd
colorsys copyreg enum graphlib heapq keyword linecache
mimetypes pydoc_data quopri stat this token types. They still
need the prerequisites in §3.
Every other public module needs some or all of the bootstrap set (§4.1), plus the natives listed for it below. An empty cell means the bootstrap set is enough.
| public module | needs beyond the bootstrap set |
|---|---|
abc |
|
annotationlib |
|
antigravity |
_signal, _struct, binascii, math, pyexpat |
argparse |
|
ast |
|
asyncio |
_signal, _socket, _string, _struct, atexit, math, select |
base64 |
binascii |
bdb |
|
bz2 |
_bz2 |
cProfile |
_lsprof |
calendar |
math |
code |
|
codecs |
|
codeop |
|
collections |
|
compileall |
_struct |
compression |
_bz2, _lzma, _struct, _zstd, zlib |
concurrent |
_interpqueues, _interpreters, _signal, _socket, _string, _struct, atexit |
configparser |
|
contextlib |
|
contextvars |
|
copy |
|
csv |
_csv |
ctypes |
_ctypes, _signal, _struct |
curses |
_curses, _curses_panel |
dataclasses |
|
datetime |
math |
dbm |
_dbm, _gdbm, _sqlite3, _struct, math |
decimal |
math |
difflib |
|
dis |
|
doctest |
_random, _sha2, _signal, _socket, _string, _struct, atexit, math, select |
email |
_random, _string, binascii, math |
encodings |
_bz2, _codecs_cn, _codecs_hk, _codecs_iso2022, _codecs_jp, _codecs_kr, _codecs_tw, _multibytecodec, binascii, unicodedata, zlib |
ensurepip |
_random, _sha2, _signal, math |
filecmp |
|
fileinput |
|
fnmatch |
|
fractions |
math |
ftplib |
_socket |
functools |
|
genericpath |
|
getopt |
|
getpass |
|
gettext |
|
glob |
|
gzip |
_struct, zlib |
hashlib |
_blake2, _md5, _sha1, _sha2, _sha3 |
hmac |
_blake2, _hmac, _md5, _sha1, _sha2, _sha3 |
html |
|
http |
_random, _socket, _string, binascii, math, select |
imaplib |
_random, _sha2, _signal, _socket, binascii, math |
importlib |
_random, _string, _struct, binascii, math |
inspect |
|
io |
|
ipaddress |
|
json |
|
locale |
|
logging |
_socket, _string, _struct, atexit, math, select |
lzma |
_lzma |
mailbox |
_random, _sha2, _socket, _string, binascii, math |
modulefinder |
|
multiprocessing |
_ctypes, _multiprocessing, _random, _signal, _socket, _struct, array, atexit, binascii, math, mmap, select |
netrc |
|
ntpath |
|
nturl2path |
|
numbers |
|
opcode |
|
operator |
|
optparse |
|
os |
|
pathlib |
|
pdb |
_random, _sha2, _signal, _socket, _string, _struct, atexit, math, select |
pickle |
_struct |
pickletools |
_struct |
pkgutil |
|
platform |
|
plistlib |
_struct, binascii, math, pyexpat |
poplib |
_socket |
posixpath |
|
pprint |
|
profile |
|
profiling |
_curses, _lsprof, _random, _remote_debugging, _signal, _socket, _struct, binascii, math, pyexpat, select |
pstats |
|
pty |
select, termios |
py_compile |
|
pyclbr |
|
pydoc |
math |
queue |
|
random |
_random, _sha2, math |
re |
|
reprlib |
|
rlcompleter |
atexit |
runpy |
|
sched |
|
secrets |
_random, _sha2, binascii, math |
selectors |
math, select |
shelve |
_struct |
shlex |
|
shutil |
|
signal |
_signal |
site |
|
smtplib |
_random, _sha2, _socket, _string, binascii, math |
socket |
_socket |
socketserver |
_socket, math, select |
sqlite3 |
_sqlite3, math |
ssl |
_socket, _ssl, binascii |
statistics |
_random, _sha2, math |
string |
_string |
stringprep |
unicodedata |
struct |
_struct |
subprocess |
_posixsubprocess, _signal, fcntl, select |
symtable |
_symtable |
sysconfig |
|
tabnanny |
|
tarfile |
_struct |
tempfile |
_random, _sha2, math |
textwrap |
|
threading |
|
timeit |
gc |
tkinter |
_tkinter |
tokenize |
|
tomllib |
math |
trace |
_struct, gc |
traceback |
|
tracemalloc |
_struct, _tracemalloc |
tty |
termios |
turtle |
_tkinter, math |
typing |
|
unittest |
_signal, _socket, _string, _struct, atexit, math |
urllib |
_random, _socket, _string, binascii, math |
uuid |
|
venv |
_signal, _string, atexit |
warnings |
|
wave |
_struct |
weakref |
|
webbrowser |
_signal, _struct, binascii, math, pyexpat |
wsgiref |
_socket, _string, binascii, math, select |
xml |
_random, _socket, _string, binascii, math, pyexpat |
xmlrpc |
_socket, _string, binascii, math, pyexpat, select |
zipapp |
_struct, binascii |
zipfile |
_struct, binascii |
zipimport |
_frozen_importlib, _frozen_importlib_external |
zoneinfo |
_struct, math |
spyc bundles a program with every module its imports reach
(build.md §3.2). Its only tree shaking is at module
level: a module that no import names is left out. That does not work on
Lib/. spyc follows imports wherever they appear, including function bodies
and branches that never run, and Lib/'s hubs import one another inside
functions:
warnings,inspect,typingandannotationliball do it;heapqimportsdoctestunderif __name__ == "__main__":.
These links tie about 315 modules together. A program that imports any one of them gets all of them.
This section designs a better cut. None of it is implemented.
The pinned Lib/ was scanned with the oracle's ast, and every import was
classified by where it sits:
| where the import sits | edges |
|---|---|
| module level | 2116 |
| function body | 407 |
lazy import at module level |
37 |
try guarded by except ImportError |
36 |
a platform test (sys.platform == "win32", os.name == "nt", ...) |
20 |
if TYPE_CHECKING: |
18 |
if __name__ == "__main__": |
11 |
The table below counts what one import bundles under each policy: modules,
then KB of source before -OO or --minify. Each column drops one more kind
of edge than the column before:
- today: follow every edge, as spyc does.
- dead blocks: skip
__main__andTYPE_CHECKINGblocks. - platforms: also skip platform tests.
- deferred: also defer function-body imports.
- module level: also defer
lazy import.
| entry | today | dead blocks | platforms | deferred | module level |
|---|---|---|---|---|---|
json |
315 / 7262 | 165 / 4279 | 162 / 4177 | 27 / 520 | 24 / 471 |
re |
315 / 7262 | 165 / 4279 | 162 / 4177 | 20 / 413 | 17 / 363 |
dataclasses |
315 / 7262 | 165 / 4279 | 162 / 4177 | 23 / 552 | 10 / 261 |
argparse |
315 / 7262 | 165 / 4279 | 162 / 4177 | 31 / 916 | 23 / 570 |
pathlib |
315 / 7262 | 165 / 4279 | 162 / 4177 | 58 / 1446 | 14 / 242 |
typing |
315 / 7262 | 165 / 4279 | 162 / 4177 | 18 / 524 | 12 / 320 |
logging |
315 / 7262 | 165 / 4279 | 162 / 4177 | 70 / 1763 | 48 / 1110 |
asyncio |
315 / 7262 | 247 / 5406 | 239 / 5306 | 128 / 2815 | 98 / 2239 |
unittest |
315 / 7262 | 258 / 5556 | 250 / 5455 | 80 / 1999 | 67 / 1633 |
For scale, all of Lib/ (608 modules, excluding tests) is 11293 KB.
In a bundled module that is not the entry, __name__ is never "__main__".
typing.TYPE_CHECKING is False at run time. An import under
if __name__ == "__main__": or if TYPE_CHECKING: can never run there, so
spyc can skip it without changing behavior. The test must be one of those
two exact forms, and the entry module keeps its __main__ block. This rule
alone halves the bundle.
Platform tests are left alone. They save three modules, and acting on them would require spyc to assume a target platform, which changes behavior when the assumption is wrong.
An import in a function body runs only when the function is called. A
lazy import runs only when its name is first used. Most of what remains
after §6.2 is reached only through such deferred edges. In the json case,
135 of the remaining 162 modules are.
spyc cannot drop them. Python reaches functions through getattr,
__dict__ and frames, so spyc cannot prove a function is never called. The
same reason keeps statement-level shaking out of spyc
(build.md §8). Instead, spyc splits the
bundle into two parts:
- Eager part. The program's C file holds the modules that the entry and its eager edges reach. These are module-level imports that are neither lazy nor in a dead block.
- Deferred part. The modules reached only through deferred edges go into
a second C file. It is built with the
-csplitting that already exists (build.md §5.2) and shares the same module table. The host links it in or chains it after the eager loader, so the program imports exactly what it imported before; only the location of the source changes.
An opt-in flag leaves the deferred part out entirely. --metafile and
--analyze then list each deferred import with the function it sits in, and
-D adds a module back. A call that reaches a missing module raises
ModuleNotFoundError, which matches how
guarded imports already treat missing modules.
spyc knows which native modules the bundled sources import. The generated
table can reference only those modules' init functions. If each native
module in libspy-std.a is its own object file, the linker drops the rest
with no further work. spy-std.c would then have to be split into one
object per module once it holds more than sys and gc.
The unit of shaking stays the module; statements inside a module are not shaken:
- top-level code can have side effects;
- module attributes are reached by name at run time.
Function-level splitting gets almost all of the gain without that risk.