Hoist import math to module scope in number.py - #393
Conversation
python-humanize#335 declared `__lazy_modules__` so that module-scope imports become lazy on Python 3.15+, but it only added the declarations — the function-level `import math` statements predating it were left in place. Those now cost a `sys.modules` lookup on every call for no benefit, since `math` is already resident by the time `humanize` is imported. Move the import to module scope and add "math" to `__lazy_modules__`, matching what filesize.py already does for the same module.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #393 +/- ##
==========================================
- Coverage 99.56% 99.56% -0.01%
==========================================
Files 12 12
Lines 927 919 -8
==========================================
- Hits 923 915 -8
Misses 4 4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will improve performance by 13.13%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | test_clamp |
63.8 µs | 56.4 µs | +13.13% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing g4lb:perf-hoist-math-import (2bdb670) with main (3201e70)
I see the deferred #397 defers it there too, and means we also get the benefit for 3.14 and older. We don't get the improvements from this PR, avoiding the |
import math to module scope in number.pyimport math to module scope in number.py
What
#335 added
__lazy_modules__so that module-scope imports become lazy on Python3.15+. It added the declarations only — the function-level
import mathstatementsthat predated it were left in place. This finishes that migration for
number.py:hoists the nine function-level
import mathstatements to module scope, and adds"math"to__lazy_modules__, matching whatfilesize.pyalready does for the verysame module.
Why
A function-level
importstill costs asys.moduleslookup and a name binding onevery call. It buys nothing here, because
mathis already resident by the timehumanizefinishes importing —python -X importtime -c "import humanize"showsmathloading at ~425 µs, beforehumanize.i18n.Measured with
pytest-benchmarkon the repository's owntests/test_benchmarks.py(Apple Silicon, CPython 3.14.6, medians of interleaved runs):
test_clamptest_apnumbertest_ordinaltest_scientifictest_intwordtest_metricGeometric mean across all 15 benchmarks: 0.9409 (−5.9 %), no benchmark regressed.
Import time
Unchanged, as expected given
mathis already loaded:(
import humanizein a fresh interpreter, best of five, three repetitions.)Checks
pytest tests/→ 724 passed, 74 skipped.ruff check,black --checkandmypyall clean on the changed file.
Not included
time.pyhas the same leftover pattern withimport datetime as dtin eightfunctions. It is a real win on the time benchmarks (
test_naturaltime−13.6 %,test_naturalday−6.6 %,test_naturaldate−6.3 %) but only −0.75 % on the geomeanacross all 15, so I have left it out to keep this change to one idea. Happy to open it
separately if you would like it.
How this was found
By pointing autor3search-python at
humanize— a harness that proposes a change, measures it against a pinned baselinewith interleaved A/B rounds, and keeps or discards it on the numbers.
humanizewasthe first real library it has been run against. Every figure above is a measurement,
not an estimate.