ADR-0009: Subpath Exports Architecture
ADR-0009: Subpath Exports Architecture
Status
Accepted (supersedes ADR-0002)
Context
In ADR-0002, we decided to use a single py.* namespace
object for all runtime functions. While this provided discoverability, it had drawbacks:
- Global namespace pollution: Functions like
dump,loads,matchin a flat namespace are ambiguous - which module do they come from? - Poor tree-shaking: Even though individual modules are tree-shakeable, the generated code
imported everything via the
pynamespace - Non-idiomatic TypeScript: Modern TypeScript prefers explicit, granular imports over namespace objects
- Bundle size concerns: Users importing
pyget all modules, even if only usingitertools
The Node.js ecosystem has evolved with subpath exports, allowing packages to expose multiple entry points cleanly.
Decision
We adopt a subpath exports architecture where:
-
Module functions are imported from subpaths:
import { chain, combinations } from "pythonlib/itertools"import { loads, dumps } from "pythonlib/json"import { search, match } from "pythonlib/re" -
Builtins remain in the main export:
import { len, range, sorted, enumerate, zip } from "pythonlib" -
Generated code uses direct function calls (not namespace-prefixed):
// Before (ADR-0002)import { py } from "pythonlib"let result = py.itertools.chain([1, 2], [3, 4])// After (ADR-0009)import { chain } from "pythonlib/itertools"let result = chain([1, 2], [3, 4]) -
Module namespace imports provide an alternative style:
import { itertools, json, re } from "pythonlib"itertools.chain([1, 2], [3, 4])
Import Categories
| Category | Import Path | Examples |
|---|---|---|
| Builtins | pythonlib | len, range, sorted, enumerate |
| Core operations | pythonlib | floorDiv, mod, pow, slice |
| Collection types | pythonlib | list, dict, set, tuple |
| itertools | pythonlib/itertools | chain, combinations, zipLongest |
| functools | pythonlib/functools | partial, reduce, lruCache |
| collections | pythonlib/collections | Counter, defaultdict, deque |
| math | pythonlib/math | sqrt, floor, ceil, pi, e |
| random | pythonlib/random | randInt, choice, shuffle |
| datetime | pythonlib/datetime | datetime, date, time, timedelta |
| json | pythonlib/json | loads, dumps, load, dump |
| re | pythonlib/re | search, match, findAll, sub |
| os | pythonlib/os | path, getCwd, sep |
| string | pythonlib/string | Template, capWords, asciiLowercase |
Note: All function names use camelCase to follow TypeScript conventions. See ADR-0011 for the naming rationale.
Generator Implementation
The generator tracks used functions with a module/function format:
// Internal trackingctx.usesRuntime.add("itertools/chain")ctx.usesRuntime.add("json/loads")ctx.usesRuntime.add("len") // Builtins without prefix
// Generated imports (grouped by module)import { len, range } from "pythonlib"import { chain, combinations } from "pythonlib/itertools"import { loads } from "pythonlib/json"Consequences
Positive
- Explicit dependencies: Clear which module each function comes from
- Better tree-shaking: Bundlers can eliminate unused modules entirely
- Smaller bundles: Import only what you use
- Idiomatic TypeScript: Matches modern ES module conventions
- IDE support: Better autocomplete and go-to-definition
- Cleaner generated code: No
py.prefix everywhere
Negative
- More import statements: Generated code may have multiple imports
- Learning curve: Users need to know which subpath contains which function
Example Transformation
Python:
from itertools import chainfrom json import loadsresult = list(chain([1, 2], loads("[3, 4]")))Generated TypeScript:
import { list } from "pythonlib"import { chain } from "pythonlib/itertools"import { loads } from "pythonlib/json"
let result = list(chain([1, 2], loads("[3, 4]")))