xtr-logging-contracts
The logging contract, and nothing else — so a library that logs installs nothing else.
Why?
A library that logs should not decide where records go. It takes a LoggerInterface, defaults to
a NullLogger, and leaves handlers, formatters and configuration to the application that wires
it.
Which means a library needs the contract, not an implementation — and should not pay for one. Depending on xtr-logging to annotate one parameter drags in a JSON codec, a clock and seventeen handlers the library never touches. This package is that dependency, reduced to what the seam is actually made of:
- 🧩
LoggerInterface— eight severities pluslog(), with a context mapping. - 🕳️
NullLogger— what makes logging optional for your callers. - 🪜
Level— the eight RFC 5424 severities, comparable as integers. - 🗂️
Context— the mapping every method takes, and the key an exception goes under. - 🪶 One dependency —
typing-extensions, for@overrideon 3.11.
from xtr_logging_contracts import LoggerInterface, NullLogger
class Checkout:
def __init__(self, logger: LoggerInterface | None = None) -> None:
self._logger = logger or NullLogger()
def pay(self, order: Order) -> None:
self._logger.error("payment {order} failed", {"order": order.id})
Install
uv add xtr-logging-contracts
Requires Python 3.11+.
Who installs what
| Depends on | |
|---|---|
| A library that logs | xtr-logging-contracts at runtime, xtr-logging as a dev dependency — its tests build real loggers and assert on a TestHandler. |
| An application | xtr-logging, which implements this contract and wires channels, handlers and processors from configuration. |
xtr-logging re-exports every symbol here rather than redefining it, so
xtr_logging.LoggerInterface is xtr_logging_contracts.LoggerInterface. That identity is what lets
a container register a logger under the interface and a library, which never imported
xtr-logging, receive it.
The interface
class LoggerInterface(Protocol):
def emergency(self, message: str, /, context: Context | None = None) -> None: ...
def alert(self, message: str, /, context: Context | None = None) -> None: ...
def critical(self, message: str, /, context: Context | None = None) -> None: ...
def error(self, message: str, /, context: Context | None = None) -> None: ...
def warning(self, message: str, /, context: Context | None = None) -> None: ...
def notice(self, message: str, /, context: Context | None = None) -> None: ...
def info(self, message: str, /, context: Context | None = None) -> None: ...
def debug(self, message: str, /, context: Context | None = None) -> None: ...
def log(self, level: LevelLike, message: str, /, context: Context | None = None) -> None: ...
The rules:
contextis a mapping of anything. Formatters describe what they cannot serialise; a value never makes logging fail.- An exception to report goes under
context["exception"]. {key}placeholders in the message are filled from context downstream, not by the logger, so a handler can still see the template and the values apart.
AbstractLogger implements the eight severity methods on top of log(), so an implementation
writes one method. LoggerAware gives a class a logger that is a NullLogger until
set_logger() is called.
ruff's
PLE1205assumes everylogger.info(...)is the standard library's and flags the context mapping as a stray format argument. Ignore it in projects using this interface.
Levels
| Level | Value | RFC 5424 | Level | Value | RFC 5424 | |
|---|---|---|---|---|---|---|
DEBUG |
100 | 7 | ERROR |
400 | 3 | |
INFO |
200 | 6 | CRITICAL |
500 | 2 | |
NOTICE |
250 | 5 | ALERT |
550 | 1 | |
WARNING |
300 | 4 | EMERGENCY |
600 | 0 |
Anywhere a level is accepted, Level.parse reads it: a Level, its value, an RFC 5424 severity,
or a name in any case ("error"). Anything else raises InvalidLevelError.
Context
Context is Mapping[str, object] — the second argument to every method above. Values are
object because a caller may log anything; whatever writes the record normalises what it cannot
serialise rather than refusing it. An exception to report goes under EXCEPTION_KEY:
from xtr_logging_contracts import EXCEPTION_KEY
logger.error("payment failed", {"order": order.id, EXCEPTION_KEY: error})
What is not here
Everything that acts on what was logged: LogRecord, Logger, LoggerFactory,
LoggingConfig, the seventeen handlers, the processors, the formatters, ambient
bound_context(), and the standard-library bridge. All of that is
xtr-logging.
LogRecord in particular belongs there, not here: it appears in no signature above. A library
that logs never builds one or sees one — it is made inside the logger and consumed by handlers
and processors, which are implementation.
HandlerInterface, ProcessorInterface and FormatterInterface are deliberately absent for the
same reason, plus one more: nothing outside xtr-logging implements them yet, and a contract
package earns its stability by staying small. They move here when a third-party handler needs
them — and LogRecord would move with them.
Errors
| Error | Raised when |
|---|---|
LoggingError |
Never directly — the base every logging error derives from, xtr-logging's included |
InvalidLevelError |
A value names no level (also a ValueError) |
Development
Developed in the python-xtr monorepo, under
packages/xtr-logging-contracts; run the commands below from there. The python-xtr-logging-contracts repository is a
read-only copy, so send issues and pull requests to the monorepo.
uv sync
uv run ruff check . && uv run ruff format --check .
uv run basedpyright
uv run ty check
uv run pytest
License
MIT — see LICENSE.
Release files for xtr-logging-contracts 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xtr_logging_contracts-1.0.1.tar.gz | 9.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xtr_logging_contracts-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.7 kB
Release files / xtr_logging_contracts-1.0.1.tar.gz
| Download URL | xtr_logging_contracts-1.0.1.tar.gz |
|---|---|
| Size | 9.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6e440060487237dad08b0bd7401490054982514ea7a943ce05bad0a9d397b1ff
|
|
BLAKE2b-256 checksum How to use checksums |
037d37d8b5b04baa1de1acbdcbad3d737e700c81740757d5b7cc6eba3b90678c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / xtr_logging_contracts-1.0.1-py3-none-any.whl
| Download URL | xtr_logging_contracts-1.0.1-py3-none-any.whl |
|---|---|
| Size | 13.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c37d02079c9dd17eec001eb57db1f0516c1b523f17037d87a33a5d25bb6881dd
|
|
BLAKE2b-256 checksum How to use checksums |
16812fc9a2c83a25c8a92d27b50736384be6dcc91d03a55fbf357c3c658566ed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log