Nullscope
Zero-cost telemetry for Python. No-op when disabled, rich context when enabled.
Why Nullscope?
Most telemetry libraries have runtime cost even when you don't need them. Nullscope is different:
- Disabled: Returns a singleton no-op object. No allocations, no timing calls, no overhead.
- Enabled: Full-featured timing and metrics with automatic scope hierarchy.
This makes Nullscope ideal for libraries (users can enable telemetry if they want) and applications where you want zero production overhead but rich debugging capability.
from nullscope import TelemetryContext
# When NULLSCOPE_ENABLED != "1", this is literally just returning a cached object
telemetry = TelemetryContext()
with telemetry("database.query"): # No-op when disabled
results = db.execute(query)
What Nullscope Is Not
-
A distributed tracing system. No trace propagation, no span IDs, no context injection for cross-service correlation. If you need that, use OpenTelemetry directly. Nullscope can feed OTel, but it doesn't replace it.
-
A metrics aggregation layer. Nullscope reports raw events to reporters. It doesn't compute percentiles, histograms, or roll up data. That's the reporter's job (or the backend's).
-
Auto-instrumentation. Nullscope won't patch your HTTP client or database driver. You instrument what you want, explicitly.
-
A logging framework. Scopes are for timing and metrics, not structured log events. (Though a reporter could emit logs.)
Installation
pip install nullscope
With OpenTelemetry support:
pip install nullscope[otel]
Quick Start
import os
os.environ["NULLSCOPE_ENABLED"] = "1" # Enable telemetry
from nullscope import TelemetryContext, SimpleReporter
# Create a reporter to see output
reporter = SimpleReporter()
telemetry = TelemetryContext(reporter)
# Time operations with automatic hierarchy
with telemetry("request"):
with telemetry("auth"):
validate_token()
with telemetry("handler"):
process_data()
# See what was collected
reporter.print_report()
Output:
=== Nullscope Report ===
--- Timings ---
request | Calls: 1 | Avg: 0.0250s | Total: 0.0250s
auth | Calls: 1 | Avg: 0.0012s | Total: 0.0012s
handler | Calls: 1 | Avg: 0.0234s | Total: 0.0234s
Configuration
| Environment Variable | Description |
|---|---|
NULLSCOPE_ENABLED=1 |
Enable telemetry (default: disabled) |
NULLSCOPE_STRICT=1 |
Enforce strict dotted scope names |
Note: environment flags are read at import time. In tests, reload nullscope after changing env vars.
API
TelemetryContext
from nullscope import TelemetryContext
telemetry = TelemetryContext() # Uses default SimpleReporter when enabled
telemetry = TelemetryContext(my_reporter) # Custom reporter
telemetry = TelemetryContext(reporter1, reporter2) # Multiple reporters
Scopes (Timing)
with telemetry("operation"):
do_work()
# With metadata
with telemetry("http.request", method="GET", path="/api/users"):
handle_request()
Decorators
@telemetry.timed("http.handler")
def handle() -> None:
process_request()
@telemetry.timed("db.query", table="users")
async def fetch_users() -> list[dict]:
return await db.fetch_all()
Metrics
telemetry.count("cache.hit") # Increment counter
telemetry.count("items.processed", 5) # Increment by N
telemetry.gauge("queue.depth", len(queue)) # Point-in-time value
telemetry.metric("custom", value, metric_type="counter") # Generic
Check Status
if telemetry.is_enabled:
# Do expensive debug logging
pass
Reporter Lifecycle
# Flush buffered reporters (if they implement flush())
telemetry.flush()
# Shutdown reporters cleanly (if they implement shutdown())
telemetry.shutdown()
Async Safety
Nullscope uses contextvars, so each async task keeps its own scope stack without cross-talk:
import asyncio
async def worker(task_id: int):
with telemetry("task", task_id=task_id):
await asyncio.sleep(0.1)
OpenTelemetry Adapter
Export to OpenTelemetry backends:
from nullscope import TelemetryContext
from nullscope.adapters.opentelemetry import OTelReporter
# Configure OTel SDK first (providers, exporters, etc.)
# Then use Nullscope with OTel reporter
telemetry = TelemetryContext(OTelReporter(service_name="my-service"))
The adapter emits:
- Timings → Histogram (seconds) + synthetic Span when wall-clock bounds are present
- Counters → Counter
- Gauges → Histogram (sampled values, since Python OTel sync gauge support is limited)
Documentation
- Design - Architecture and implementation details
- Examples - Real-world usage patterns
- Comparison - When to use Nullscope vs alternatives
- Roadmap - Version milestones and planned features
License
Metadata
Release files for nullscope 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nullscope-0.3.0.tar.gz | 51.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nullscope-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 62.9 kB
Release files / nullscope-0.3.0.tar.gz
| Download URL | nullscope-0.3.0.tar.gz |
|---|---|
| Size | 51.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c1681b769dc732512725cf6f1729b5be9195153f5fe7df96c1a9538a2df2403a
|
|
BLAKE2b-256 checksum How to use checksums |
5f844d9725b04a076dae684ece050101618d783bed809db6b82f08ab6bb657cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / nullscope-0.3.0-py3-none-any.whl
| Download URL | nullscope-0.3.0-py3-none-any.whl |
|---|---|
| Size | 11.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
23fbb335555f00c61780f934c0efba1936540cd8e979cf7cc9c476decebc3cd0
|
|
BLAKE2b-256 checksum How to use checksums |
f4796aae7714de60fc85d569472f0fd557e921924042932c00b1889da7aa0ec5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|