Skip to content

Hoist import math to module scope in number.py - #393

Open
g4lb wants to merge 1 commit into
python-humanize:mainfrom
g4lb:perf-hoist-math-import
Open

Hoist import math to module scope in number.py#393
g4lb wants to merge 1 commit into
python-humanize:mainfrom
g4lb:perf-hoist-math-import

Conversation

@g4lb

@g4lb g4lb commented Sep 7, 2026

Copy link
Copy Markdown

What

#335 added __lazy_modules__ so that module-scope imports become lazy on Python
3.15+. It added the declarations only — the function-level import math statements
that predated it were left in place. This finishes that migration for number.py:
hoists the nine function-level import math statements to module scope, and adds
"math" to __lazy_modules__, matching what filesize.py already does for the very
same module.

Why

A function-level import still costs a sys.modules lookup and a name binding on
every call. It buys nothing here, because math is already resident by the time
humanize finishes importing — python -X importtime -c "import humanize" shows
math loading at ~425 µs, before humanize.i18n.

Measured with pytest-benchmark on the repository's own tests/test_benchmarks.py
(Apple Silicon, CPython 3.14.6, medians of interleaved runs):

benchmark before after change
test_clamp 177.1 ns 134.9 ns −23.8 %
test_apnumber 304.7 ns 252.0 ns −17.3 %
test_ordinal 417.7 ns 368.7 ns −11.7 %
test_scientific 458.0 ns 417.0 ns −9.0 %
test_intword 958.0 ns 875.0 ns −8.7 %
test_metric 500.0 ns 459.0 ns −8.2 %

Geometric mean across all 15 benchmarks: 0.9409 (−5.9 %), no benchmark regressed.

Import time

Unchanged, as expected given math is already loaded:

  • before: 1.64 / 1.55 / 1.58 ms
  • after: 1.56 / 1.51 / 1.58 ms

(import humanize in a fresh interpreter, best of five, three repetitions.)

Checks

pytest tests/ → 724 passed, 74 skipped. ruff check, black --check and mypy
all clean on the changed file.

Not included

time.py has the same leftover pattern with import datetime as dt in eight
functions. 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 geomean
across 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 baseline
with interleaved A/B rounds, and keeps or discards it on the numbers. humanize was
the first real library it has been run against. Every figure above is a measurement,
not an estimate.

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

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.56%. Comparing base (3201e70) to head (2bdb670).

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              
Flag Coverage Δ
macos-latest 97.60% <100.00%> (-0.03%) ⬇️
ubuntu-latest 97.60% <100.00%> (-0.03%) ⬇️
windows-latest 95.42% <100.00%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq

codspeed-hq Bot commented Sep 8, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 13.13%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 14 untouched benchmarks

Performance Changes

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)

Open in CodSpeed

@hugovk

hugovk commented Sep 8, 2026

Copy link
Copy Markdown
Member

A function-level import still costs a sys.modules lookup and a name binding on
every call. It buys nothing here, because math is already resident by the time
humanize finishes importing — python -X importtime -c "import humanize" shows
math loading at ~425 µs, before humanize.i18n.

I see the deferred math import had no effect because we were still importing it eagerly elsewhere.

#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 cache lookup, but these are all small numbers so it's okay.

@hugovk hugovk changed the title perf: hoist import math to module scope in number.py Hoist import math to module scope in number.py Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: Changed For changes in existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants