Skip to content

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:

  1. Global namespace pollution: Functions like dump, loads, match in a flat namespace are ambiguous - which module do they come from?
  2. Poor tree-shaking: Even though individual modules are tree-shakeable, the generated code imported everything via the py namespace
  3. Non-idiomatic TypeScript: Modern TypeScript prefers explicit, granular imports over namespace objects
  4. Bundle size concerns: Users importing py get all modules, even if only using itertools

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:

  1. Module functions are imported from subpaths:

    import { chain, combinations } from "pythonlib/itertools"
    import { loads, dumps } from "pythonlib/json"
    import { search, match } from "pythonlib/re"
  2. Builtins remain in the main export:

    import { len, range, sorted, enumerate, zip } from "pythonlib"
  3. 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])
  4. Module namespace imports provide an alternative style:

    import { itertools, json, re } from "pythonlib"
    itertools.chain([1, 2], [3, 4])

Import Categories

CategoryImport PathExamples
Builtinspythonliblen, range, sorted, enumerate
Core operationspythonlibfloorDiv, mod, pow, slice
Collection typespythonliblist, dict, set, tuple
itertoolspythonlib/itertoolschain, combinations, zipLongest
functoolspythonlib/functoolspartial, reduce, lruCache
collectionspythonlib/collectionsCounter, defaultdict, deque
mathpythonlib/mathsqrt, floor, ceil, pi, e
randompythonlib/randomrandInt, choice, shuffle
datetimepythonlib/datetimedatetime, date, time, timedelta
jsonpythonlib/jsonloads, dumps, load, dump
repythonlib/research, match, findAll, sub
ospythonlib/ospath, getCwd, sep
stringpythonlib/stringTemplate, 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 tracking
ctx.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 chain
from json import loads
result = 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]")))