Skip to content

Defer inspect import to reduce import time by ~25%#1547

Open
KRRT7 wants to merge 2 commits intopython-attrs:mainfrom
KRRT7:perf/defer-inspect-import
Open

Defer inspect import to reduce import time by ~25%#1547
KRRT7 wants to merge 2 commits intopython-attrs:mainfrom
KRRT7:perf/defer-inspect-import

Conversation

@KRRT7
Copy link
Copy Markdown

@KRRT7 KRRT7 commented Apr 21, 2026

Summary

  • Defer import inspect from module level to first use in _compat.py and _make.py
  • Lazy-load converters and validators submodules from attr/__init__.py and attrs/__init__.py

Why

inspect costs ~12ms to import (it pulls in ast, re, enum, dis, tokenize) but is only used in three places, all at class-build time — never at module scope:

File Location Usage
_compat.py _AnnotationExtractor.__init__() inspect.signature() for converter type extraction
_make.py line 709 inspect.signature() for __attrs_pre_init__ arg detection
_make.py line 931 inspect.signature() for cached_property return annotations

The eager import of inspect is triggered at import attr time because attr/__init__.py eagerly imports validators, which uses @attrs() to define validator classes at module level, which triggers class building → Converter()_AnnotationExtractor()import inspect.

By deferring inspect into the methods that use it and lazy-loading converters/validators, the inspect import is deferred until the first attrs class is actually built by user code.

Changes

  1. _compat.py: Remove module-level import inspect, add it inside _AnnotationExtractor.__init__(), get_first_param_type(), and get_return_type()
  2. _make.py: Remove module-level import inspect, add it inside the two methods that use it (lines 709, 931)
  3. attr/__init__.py: Move converters and validators from eager import to lazy-load via existing __getattr__
  4. attrs/__init__.py: Same lazy-load for converters and validators

Benchmark

Controlled environment — Azure Standard_D2s_v5, Intel Xeon Platinum 8370C @ 2.80GHz, Python 3.13.13, Ubuntu 24.04, 100 runs, 10 warmup via hyperfine:

With bytecode Without bytecode
Baseline (main) 42.1ms ± 1.5ms 42.6ms ± 1.1ms
Optimized 31.5ms ± 1.0ms 30.8ms ± 1.2ms
Improvement 10.6ms (25%) 11.8ms (28%)
Reproduce with hyperfine

Requires hyperfine.

# Clone and set up
git clone https://github.com/KRRT7/attrs.git && cd attrs
uv venv --python 3.13 && uv pip install -e .

# Measure optimized (this branch)
git checkout perf/defer-inspect-import
uv pip install -e .
hyperfine --warmup 10 --runs 100 ".venv/bin/python -c 'import attrs'"

# Measure baseline (main)
git checkout main
uv pip install -e .
hyperfine --warmup 10 --runs 100 ".venv/bin/python -c 'import attrs'"

# Without bytecode caching (cleaner signal)
hyperfine --warmup 10 --runs 100 \
    "PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -c 'import attrs'"
Reproduce with -X importtime
# Check if inspect is loaded at import time
python -X importtime -c "import attr" 2>&1 | grep inspect

# On main (baseline): will show inspect (~12ms cumulative)
# On this branch: no output (inspect deferred)

# Full breakdown sorted by cumulative cost
python -X importtime -c "import attrs" 2>&1 | sort -t'|' -k2 -rn | head -15

# Quick Python script for median over N runs:
python -c "
import subprocess, sys, statistics

def measure(stmt, n=20):
    times = []
    for _ in range(n):
        r = subprocess.run(
            [sys.executable, '-X', 'importtime', '-c', stmt],
            capture_output=True, text=True,
        )
        for line in r.stderr.strip().split('\n'):
            if '|' not in line:
                continue
            parts = line.split('|')
            try:
                cum = int(parts[1].strip())
            except ValueError:
                continue
        times.append(cum)
    return statistics.median(times), statistics.stdev(times)

med, sd = measure('import attrs')
print(f'import attrs: median={med:.0f}us  stdev={sd:.0f}us')
med, sd = measure('import attr')
print(f'import attr:  median={med:.0f}us  stdev={sd:.0f}us')
"

Test plan

  • Full test suite: 1385 passed, 5 skipped, 1 xfailed (macOS, Python 3.13.7)
  • Full test suite: 1115 passed, 1 skipped (Azure VM, Linux, Python 3.13.13; 2 pre-existing failures in test_packaging.py due to dev version string parsing — same on main)
  • attr.validators and attr.converters still accessible via lazy load
  • inspect still imported before any attrs class is built (deferred, not removed)
  • No behavioral change — public API identical

KRRT7 and others added 2 commits April 21, 2026 01:50
Move `import inspect` from module level in `_compat.py` and
`_make.py` into the functions that use it. Lazy-load `converters`
and `validators` submodules from `attr/__init__.py` and
`attrs/__init__.py` since they trigger class building (which
invokes inspect) at import time.

inspect is only used in three places, all at class-build time:
- _compat._AnnotationExtractor.__init__() for signature extraction
- _make.py line 709 for __attrs_pre_init__ arg detection
- _make.py line 931 for cached_property return annotations

Benchmark (Python 3.13, PYTHONDONTWRITEBYTECODE=1, 50 runs):
- Before: 85.0ms ± 20.4ms
- After:  64.7ms ± 12.0ms
- Improvement: ~20ms (24%)

All 1385 tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant