A minimal, zero-dependency Python decorator for measuring how long your functions take to run.
Project description
howlong-pyalp
A minimal, zero-dependency Python decorator for measuring how long your functions take to run.
from howlong_pyalp import howlong
@howlong
def process_data(n):
return sum(i ** 2 for i in range(n))
process_data(1_000_000)
# [process_data] 142.871 milliseconds
Table of Contents
- Why howlong?
- Features
- Installation
- Quick Start
- Usage
- API Reference
- How It Works
- Examples
- Caveats & Limitations
- Requirements
- Contributing
- Changelog
- License
Why howlong?
Profiling tools like cProfile are powerful but heavy-handed when all you want to know is "how long did this function take?". The timeit module is excellent for micro-benchmarks but verbose for ad-hoc timing. Manually calling time.perf_counter() before and after a function is repetitive and easy to get wrong.
howlong solves this with a single decorator:
- Apply
@howlongabove any function — that's it. - Get readable, formatted output on every call.
- Choose between wall-clock-like and CPU-only measurement.
- Pick the time unit, or let it choose automatically.
No setup, no configuration files, no dependencies.
Features
- Single-line integration. Drop
@howlongon any function. - Three calling forms.
@howlong,@howlong(), and@howlong(unit="ms")all work. - Two measurement modes. Wall-clock-like elapsed time (
perf) or pure CPU time (cpu). - Flexible units. Seconds, milliseconds, microseconds, nanoseconds — or
"auto"for the most readable one. - Preserves function metadata. Name, docstring, type hints, and signature are kept intact via
functools.wraps. - Exception-safe. Timing is always reported, even if the wrapped function raises.
- Zero dependencies. Pure standard library — no compatibility headaches.
- Tiny footprint. A single file, well under 100 lines of code.
Installation
Install from PyPI using pip:
pip install howlong-pyalp
To install from source:
git clone https://github.com/BYALPERENK/howlong-pyalp.git
cd howlong-pyalp
pip install .
Note: The package is named
howlong-pyalpon PyPI (with a hyphen) but imported ashowlong_pyalp(with an underscore). This is a Python naming convention — hyphens are not valid in module names.
Quick Start
from howlong_pyalp import howlong
@howlong
def my_function():
total = 0
for i in range(100_000):
total += i * i
return total
my_function()
# [my_function] 6.734 milliseconds
That's it. The function still works exactly as before, but now prints how long each call took.
Usage
Three calling forms
The decorator can be applied in three equivalent ways:
from howlong_pyalp import howlong
# 1. Bare — no parentheses, default settings
@howlong
def f(): ...
# 2. Parenthesized — same as bare, but explicit
@howlong()
def f(): ...
# 3. Configured — with one or more parameters
@howlong(unit="us")
def f(): ...
@howlong(mode="cpu", unit="ms")
def f(): ...
All three return a decorated function whose runtime is printed on every call. The bare form is convenient; the parenthesized form is useful when you want a consistent style across your codebase.
Choosing a time unit
The unit parameter controls how elapsed time is formatted in the output.
| Value | Output example |
|---|---|
"s" |
[f] 1.245 seconds |
"ms" |
[f] 1245.388 milliseconds (default) |
"us" |
[f] 1245388.000 microseconds |
"ns" |
[f] 1245388012 nanoseconds |
"auto" |
Picks the most readable unit based on the actual elapsed time |
The default unit is "ms", which is appropriate for most functions. For short snippets or micro-benchmarks, switch to "us" or "ns". For long-running tasks, "s" reads more naturally. When the runtime is unpredictable, "auto" adapts automatically:
@howlong(unit="auto")
def adaptive(n):
return sum(range(n))
adaptive(100)
# [adaptive] 12.413 microseconds
adaptive(10_000_000)
# [adaptive] 2.108 seconds
Choosing a measurement mode
The mode parameter selects which underlying clock is used.
| Value | Underlying clock | What it measures |
|---|---|---|
"perf" |
time.perf_counter |
Wall-clock-like elapsed time, including sleeps and I/O |
"cpu" |
time.process_time |
CPU time only — sleeps and I/O are excluded |
The default is "perf", which answers the question "How long did the user wait?".
Use "cpu" when you want to measure how hard the CPU actually worked, ignoring time spent waiting for the disk, the network, or time.sleep(). This is useful for comparing the computational cost of algorithms regardless of system load or I/O variance.
import time
from howlong_pyalp import howlong
@howlong(mode="perf")
def with_sleep():
time.sleep(0.5)
return sum(i * i for i in range(100_000))
@howlong(mode="cpu")
def with_sleep_cpu():
time.sleep(0.5)
return sum(i * i for i in range(100_000))
with_sleep()
# [with_sleep] 505.088 milliseconds
with_sleep_cpu()
# [with_sleep_cpu] 4.913 milliseconds
Notice the dramatic difference: perf includes the 500ms sleep, while cpu reports only the time the CPU actually spent computing.
API Reference
howlong(func=None, *, unit="ms", mode="perf")
Decorator that measures and prints the runtime of the wrapped function.
Parameters
-
func(callable, optional) The function to decorate. Filled in automatically when using@howlongwithout parentheses. You should not pass this explicitly. -
unit(str, default"ms") Time unit for the printed output. One of"auto","s","ms","us","ns". RaisesValueErrorfor any other value. -
mode(str, default"perf") Which clock to use. One of"perf"or"cpu". RaisesValueErrorfor any other value.
Returns
The decorated function. The wrapped function behaves identically to the original — same arguments, same return value, same exceptions — but its runtime is printed to standard output on every call.
Output format
[function_name] <value> <unit_name>
For example: [process_data] 142.871 milliseconds
How It Works
The decorator wraps your function in a small piece of code that:
- Records the current time using the selected clock.
- Calls your function inside a
tryblock. - In the
finallyblock, computes the elapsed time and prints it.
The try/finally ensures the timing is printed even if the function raises. Exceptions are not swallowed — they propagate normally.
Two clocks are available:
-
time.perf_counteris Python's highest-resolution monotonic clock. It is not affected by system clock adjustments (such as NTP corrections) and can have nanosecond resolution depending on the platform. -
time.process_timemeasures CPU time consumed by the current process. It excludes time spent sleeping or waiting on I/O.
Function metadata (__name__, __doc__, __annotations__, __qualname__, etc.) is preserved through functools.wraps. The original undecorated function remains accessible via the __wrapped__ attribute, which makes the decorator compatible with introspection tools, test frameworks, and documentation generators such as Sphinx.
Examples
Timing a numerical computation
from howlong_pyalp import howlong
@howlong
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
fibonacci(100_000)
# [fibonacci] 89.421 milliseconds
Comparing wall time and CPU time for I/O-bound code
import requests
from howlong_pyalp import howlong
@howlong(mode="perf")
def fetch_perf():
return requests.get("https://example.com").status_code
@howlong(mode="cpu")
def fetch_cpu():
return requests.get("https://example.com").status_code
fetch_perf()
# [fetch_perf] 312.482 milliseconds <- dominated by network wait
fetch_cpu()
# [fetch_cpu] 8.221 milliseconds <- only the actual CPU work
Using "auto" for variable workloads
@howlong(unit="auto")
def variable_workload(size):
return sum(i * i for i in range(size))
variable_workload(100)
# [variable_workload] 4.812 microseconds
variable_workload(10_000_000)
# [variable_workload] 1.024 seconds
Metadata is preserved
@howlong
def documented(x: int) -> int:
"""Return the square of x."""
return x * x
print(documented.__name__) # documented
print(documented.__doc__) # Return the square of x.
print(documented.__annotations__) # {'x': int, 'return': int}
Exceptions still propagate
@howlong
def will_fail():
raise ValueError("oops")
will_fail()
# [will_fail] 0.012 milliseconds
# Traceback (most recent call last):
# ...
# ValueError: oops
The timing is still printed, and the exception is re-raised normally.
Stacking with other decorators
from functools import lru_cache
from howlong_pyalp import howlong
@howlong
@lru_cache(maxsize=128)
def expensive(n):
return sum(i ** 2 for i in range(n))
expensive(1_000_000) # First call: actual computation
# [expensive] 142.871 milliseconds
expensive(1_000_000) # Second call: served from cache
# [expensive] 0.003 milliseconds
Decorator order matters. Placing @howlong on the outside (closer to the def) means it measures the time after caching is applied — useful for seeing cache effectiveness.
Caveats & Limitations
-
Single-call measurements are noisy at the microsecond level. A single timed call of a tiny function is unreliable due to OS scheduling, garbage collection, and CPU frequency scaling. For micro-benchmarks, use Python's built-in
timeitmodule, which runs the code many times and reports statistics. -
No support for
asyncfunctions. Wrapping a coroutine function with this decorator will time how long it takes to create the coroutine, not how long the awaited operation takes. Async support is on the roadmap. -
Generators time only the call, not the iteration. A generator function returns a generator object instantly. The decorator reports the time to create that object, not the time spent consuming it.
-
Output goes to
stdoutviaprint. This is intentional for simplicity but means you cannot easily redirect or silence the output. A future version may switch tologgingfor greater flexibility. -
Not thread-safe in the sense of grouped output. If multiple threads call decorated functions concurrently, their
printstatements may interleave on the same line. The timing measurements themselves remain accurate per call. -
mode="cpu"resolution is lower thanmode="perf". Depending on the platform,process_timemay have a coarser resolution (often around 10-15 milliseconds on Windows). For sub-millisecond CPU measurements, preferperfmode.
Requirements
- Python 3.7 or newer.
- No external dependencies.
Tested on Linux, macOS, and Windows.
Contributing
Contributions are welcome. If you find a bug, have a feature request, or want to submit a pull request:
- Open an issue on GitHub describing the problem or proposal.
- Fork the repository and create a feature branch.
- Add tests for any new behavior.
- Run the test suite locally:
pip install -e ".[dev]" pytest
- Submit a pull request with a clear description of the change.
Please keep contributions consistent with the existing code style and the zero-dependency principle.
Changelog
0.1.0
- Initial release.
howlongdecorator withunitandmodeparameters.- Support for bare, parenthesized, and configured calling forms.
- Automatic unit selection via
unit="auto".
License
This project is licensed under the MIT License. See the LICENSE file for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file howlong_pyalp-0.1.0.tar.gz.
File metadata
- Download URL: howlong_pyalp-0.1.0.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06cc00a9762b1f294a1b0e19cf47a2a63f6a777b7a60c664e02ef991a565f14e
|
|
| MD5 |
e92f6d719a22fba3b6213c8430ac4f1d
|
|
| BLAKE2b-256 |
21fb4437946f9f5d50d2950577043a3f02a6258c144a2709002ad61c96fdb99a
|
File details
Details for the file howlong_pyalp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: howlong_pyalp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61e6cb2945c07720a0189266b32042825cedb7f4b303c50bf16f9fd5191ad600
|
|
| MD5 |
6611082cf5349547cde1cff3f00a9713
|
|
| BLAKE2b-256 |
9ecc8d51f7c4aa42765e57399143e3e065a1611e6c2ee90fc7215edf15ce865f
|