Import-time env capture forces lazy-import hacks; read env at call time
Import-Time Env Capture Forces Lazy-Import Hacks
Type: Principle / antipattern (no severity — applies whenever a module’s behavior is parameterized by env that callers set at runtime) · Applies to: any module read by more than one consumer that each pins its own configuration before driving it.
Problem
A module captures its configuration from os.environ at import time into
module-level constants:
_WORLD_NAME = os.environ.get("SPECTRAL_TAX_PREP_WORLD_NAME", "tax-prep-1040-ty2025")_CUSTOMER_DOMAIN_SLUG = os.environ.get("SPECTRAL_TAX_PREP_DOMAIN_SLUG", "...")But the consumers each pin their own identity right before driving the module
(the validation gate uses one domain slug; the CLI uses another). With import-time
capture, a plain cached import freezes whatever env existed when the module was
first imported anywhere in the process — usually the defaults, or another
consumer’s values. The configuration the caller just set never takes effect.
The usual “fix” papers over the symptom instead of removing the cause: a bespoke
loader (importlib.spec_from_file_location + module_from_spec + exec_module)
that re-executes the module body on every call so the just-set env is re-read. In
this codebase that loader (_import_phase_c_author) also walked the directory tree
to locate a tools/dev/*.py script by path — a packaged app (spectral_test_agents)
reaching out to a loose dev script through filesystem discovery, which is its own
smell.
Investigation
What happened (SPEC-690, af592a5a)
The tax-prep authoring harness lived at tools/dev/phase_c_author.py and captured
~10 env values at import. Two in-package consumers (the system: validation driver
and the tax-prep CLI) each did:
os.environ.update(_MY_DEDICATED_IDENTITY) # pin my own world/customer slugphase_c_author = _import_phase_c_author() # re-exec the module so the env "takes"The re-exec loader existed only because of the import-time capture. It worked,
but it was fragile (any earlier import would have frozen the wrong values) and it
forced the package to depend on a tools/dev script via a path-walk.
The trap
A normal from … import author would have broken identity: the module body runs
once, cached, with whatever env was present at first import. The behavior silently
depended on import order — undetectable until a second consumer ran in the same
process with different values.
Root Cause
Import-time os.environ capture into module-level constants. It binds
configuration to import timing rather than use, which is almost never what you
want for a module that more than one caller parameterizes. Every workaround
(lazy import, re-exec loader, “import it last”) is just managing the timing the
capture created.
Solution
Read env at call time through small accessor functions. Nothing is captured at import, so a plain cached import is import-order-independent and the loader hack deletes cleanly.
# Before — frozen at import; forces a re-exec loader to apply caller env_API_BASE = os.environ.get("SPECTRAL_API_BASE_URL", "http://127.0.0.1:8000")_WORLD_NAME = os.environ.get("SPECTRAL_TAX_PREP_WORLD_NAME", "tax-prep-1040-ty2025")
# After — resolved per call; a plain `from … import author` is order-independentdef _api_base() -> str: return os.environ.get("SPECTRAL_API_BASE_URL", "http://127.0.0.1:8000")
def _world_name() -> str: return os.environ.get("SPECTRAL_TAX_PREP_WORLD_NAME", "tax-prep-1040-ty2025")Consumer, now with a normal import:
from spectral_test_agents.tax_prep.authoring import author # cached, order-independent
os.environ.update(_VALIDATION_IDENTITY) # pin my identityawait author.ensure_deployed_world(...) # accessors read it at call timeImplementation notes
- Genuinely-constant, non-env values (timeouts, computed paths) stay module constants — only env-derived values move to accessors.
- The relocation that this enabled also put the harness in the package
(
tax_prep/authoring/), with its shared dependency (customer_seed, used by qa too) legitimately remaining intools/devand reached via the repo’s PEP420 idiomfrom tools.dev import customer_seed(pytest needspythonpath=["../.."]to resolve it under the test-agents rootdir).
Prevention
Best practices
- When a module’s behavior is parameterized by env that callers set at runtime, read env at the point of use, never into module-level constants.
- Treat a re-exec / path-walk importer as a symptom: ask “what made a normal import insufficient?” — it is almost always import-time capture upstream.
- A packaged module that must locate another module by filesystem path is a seam smell; prefer a real import (relocate code, or reach shared dev utilities via the established namespace idiom).
Warning signs
_CONST = os.environ.get(...)at module top level in a module with >1 caller.- A comment like “set the env before this import” or “import this lazily so …”.
importlib.util.spec_from_file_locationused to load first-party code.
References
- Commit
af592a5a(SPEC-690) — convertedauthor.pyto call-time accessors and deleted the_import_phase_c_authorre-exec loader. - Memory:
project_test_agents_authoring_harness_location_and_import_contract.