Skip to main content

A minimal, zero-dependency Python decorator for measuring how long your functions take to run.

Project description

howlong-pyalp

PyPI version Python versions License: MIT Downloads

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?

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 @howlong above 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 @howlong on 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-pyalp on PyPI (with a hyphen) but imported as howlong_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 @howlong without parentheses. You should not pass this explicitly.

  • unit (str, default "ms") Time unit for the printed output. One of "auto", "s", "ms", "us", "ns". Raises ValueError for any other value.

  • mode (str, default "perf") Which clock to use. One of "perf" or "cpu". Raises ValueError for 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:

  1. Records the current time using the selected clock.
  2. Calls your function inside a try block.
  3. In the finally block, 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_counter is 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_time measures 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 timeit module, which runs the code many times and reports statistics.

  • No support for async functions. 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 stdout via print. This is intentional for simplicity but means you cannot easily redirect or silence the output. A future version may switch to logging for greater flexibility.

  • Not thread-safe in the sense of grouped output. If multiple threads call decorated functions concurrently, their print statements may interleave on the same line. The timing measurements themselves remain accurate per call.

  • mode="cpu" resolution is lower than mode="perf". Depending on the platform, process_time may have a coarser resolution (often around 10-15 milliseconds on Windows). For sub-millisecond CPU measurements, prefer perf mode.


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:

  1. Open an issue on GitHub describing the problem or proposal.
  2. Fork the repository and create a feature branch.
  3. Add tests for any new behavior.
  4. Run the test suite locally:
    pip install -e ".[dev]"
    pytest
    
  5. 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.
  • howlong decorator with unit and mode parameters.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

howlong_pyalp-0.1.0.tar.gz (16.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

howlong_pyalp-0.1.0-py3-none-any.whl (9.7 kB view details)

Uploaded Python 3

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

Hashes for howlong_pyalp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 06cc00a9762b1f294a1b0e19cf47a2a63f6a777b7a60c664e02ef991a565f14e
MD5 e92f6d719a22fba3b6213c8430ac4f1d
BLAKE2b-256 21fb4437946f9f5d50d2950577043a3f02a6258c144a2709002ad61c96fdb99a

See more details on using hashes here.

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

Hashes for howlong_pyalp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 61e6cb2945c07720a0189266b32042825cedb7f4b303c50bf16f9fd5191ad600
MD5 6611082cf5349547cde1cff3f00a9713
BLAKE2b-256 9ecc8d51f7c4aa42765e57399143e3e065a1611e6c2ee90fc7215edf15ce865f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page