Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/

## [Unreleased](https://github.com/hynek/structlog/compare/26.1.0...HEAD)

### Changed

- On Python 3.15 and later, *structlog* uses [PEP 810](https://peps.python.org/pep-0810/) lazy imports for modules that are only needed once you log.
This makes `import structlog` significantly faster.


## [26.1.0](https://github.com/hynek/structlog/compare/25.5.0...26.1.0) - 2026-06-06

Expand Down
12 changes: 12 additions & 0 deletions src/structlog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@

from __future__ import annotations


__lazy_modules__ = [
"structlog",
"structlog._base",
"structlog._config",
"structlog._generic",
"structlog._native",
"structlog._output",
"structlog.exceptions",
"structlog.testing",
]

from structlog import (
contextvars,
dev,
Expand Down
3 changes: 3 additions & 0 deletions src/structlog/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from __future__ import annotations


__lazy_modules__ = ["warnings"]

import os
import sys
import warnings
Expand Down
3 changes: 3 additions & 0 deletions src/structlog/_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

from __future__ import annotations


__lazy_modules__ = ["traceback"]

import sys
import traceback

Expand Down
3 changes: 3 additions & 0 deletions src/structlog/_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from __future__ import annotations


__lazy_modules__ = ["asyncio", "threading"]

import asyncio
import collections
import contextvars
Expand Down
3 changes: 3 additions & 0 deletions src/structlog/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from __future__ import annotations


__lazy_modules__ = ["copy", "pickle", "threading"]

import copy
import sys
import threading
Expand Down
3 changes: 3 additions & 0 deletions src/structlog/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from __future__ import annotations


__lazy_modules__ = ["contextlib"]

import sys

from contextlib import suppress
Expand Down
3 changes: 3 additions & 0 deletions src/structlog/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

from __future__ import annotations


__lazy_modules__ = ["warnings"]

import sys
import warnings

Expand Down
15 changes: 9 additions & 6 deletions src/structlog/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

from __future__ import annotations

import datetime

__lazy_modules__ = ["datetime", "logging", "threading"]

import datetime as dt
import enum
import json
import logging
Expand Down Expand Up @@ -523,19 +526,19 @@ def _make_stamper(
msg = "UNIX timestamps are always UTC."
raise ValueError(msg)

now: Callable[[], datetime.datetime]
now: Callable[[], dt.datetime]

if utc:

def now() -> datetime.datetime:
return datetime.datetime.now(tz=datetime.timezone.utc)
def now() -> dt.datetime:
return dt.datetime.now(tz=dt.timezone.utc)

else:

def now() -> datetime.datetime:
def now() -> dt.datetime:
# We don't need the TZ for our own formatting. We add it only for
# user-defined formats later.
return datetime.datetime.now() # noqa: DTZ005
return dt.datetime.now() # noqa: DTZ005

if fmt is None:

Expand Down
3 changes: 3 additions & 0 deletions src/structlog/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

from __future__ import annotations


__lazy_modules__ = ["asyncio", "threading", "warnings"]

import asyncio
import contextvars
import functools
Expand Down
3 changes: 3 additions & 0 deletions src/structlog/threadlocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

from __future__ import annotations


__lazy_modules__ = ["uuid", "warnings"]

import contextlib
import sys
import threading
Expand Down
3 changes: 3 additions & 0 deletions src/structlog/tracebacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

from __future__ import annotations


__lazy_modules__ = ["traceback"]

import os
import os.path
import sys
Expand Down
14 changes: 7 additions & 7 deletions tests/processors/test_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations

import datetime
import datetime as dt
import json
import pickle

Expand Down Expand Up @@ -331,7 +331,7 @@ def test_FallbackEncoder_falls_back(self):
The fallback handler uses repr if it doesn't know the type.
"""
s = json.dumps(
{"date": datetime.date(1980, 3, 25)},
{"date": dt.date(1980, 3, 25)},
default=_json_fallback_handler,
)

Expand All @@ -351,7 +351,7 @@ def test_custom_fallback(self):
A custom fallback handler can be used.
"""
jr = JSONRenderer(default=lambda x: repr(x)[::-1])
d = {"date": datetime.date(1980, 3, 25)}
d = {"date": dt.date(1980, 3, 25)}

assert '{"date": ")52 ,3 ,0891(etad.emitetad"}' == jr(None, None, d)

Expand Down Expand Up @@ -411,7 +411,7 @@ def test_formats(self):
assert "1980" == d["timestamp"]

@time_machine.travel(
datetime.datetime(1980, 3, 25, 16, 0, 0, tzinfo=datetime.timezone.utc)
dt.datetime(1980, 3, 25, 16, 0, 0, tzinfo=dt.timezone.utc)
)
def test_inserts_formatted_utc(self):
"""
Expand All @@ -428,7 +428,7 @@ def test_inserts_formatted_local(self):
"""
The fmt string in local timezone works.
"""
local_tz = datetime.datetime.now().astimezone().tzname()
local_tz = dt.datetime.now().astimezone().tzname()
ts = TimeStamper(fmt="%Y-%m-%d %H:%M:%S %Z", utc=False)
d = ts(None, None, {})

Expand All @@ -442,11 +442,11 @@ def test_tz_aware(self):
ts = TimeStamper(fmt="%z")
d = ts(None, None, {})

assert "" == datetime.datetime.now().strftime("%z") # noqa: DTZ005
assert "" == dt.datetime.now().strftime("%z") # noqa: DTZ005
assert "" != d["timestamp"]

@time_machine.travel(
datetime.datetime(1980, 3, 25, 16, 0, 0, tzinfo=datetime.timezone.utc)
dt.datetime(1980, 3, 25, 16, 0, 0, tzinfo=dt.timezone.utc)
)
def test_adds_Z_to_iso(self):
"""
Expand Down
29 changes: 29 additions & 0 deletions tests/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# 2.0, and the MIT License. See the LICENSE file in the root of this
# repository for complete details.

import json
import subprocess
import sys

from importlib import metadata

import pytest
Expand Down Expand Up @@ -47,3 +51,28 @@ def test_does_not_exist(self):
AttributeError, match="module structlog has no attribute __yolo__"
):
structlog.__yolo__


@pytest.mark.skipif(
sys.version_info < (3, 15), reason="PEP 810 lazy imports need Python 3.15+"
)
class TestLazyImports:
def test_import_does_not_load_asyncio(self):
"""
Importing structlog in a fresh interpreter does not import asyncio,
because modules that are only needed when logging are imported lazily.
"""
script = (
"import json, sys\n"
"before = set(sys.modules)\n"
"import structlog\n"
"print(json.dumps(sorted(set(sys.modules) - before)))\n"
)
out = subprocess.run(
[sys.executable, "-c", script],
check=True,
capture_output=True,
text=True,
).stdout

assert "asyncio" not in json.loads(out)
Loading