Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.2
rev: v0.11.4
hooks:
- id: ruff
args: [--fix, --show-fixes]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies: [ "typing_extensions" ]
Expand Down
10 changes: 4 additions & 6 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ build:
os: ubuntu-22.04
tools:
python: "3.11"
jobs:
install:
- python -m pip install --no-cache-dir "pip >= 25.1"
- python -m pip install --upgrade --upgrade-strategy only-if-needed --no-cache-dir --group doc .

sphinx:
configuration: docs/conf.py
fail_on_warning: true

python:
install:
- method: pip
path: .
extra_requirements: [doc]
15 changes: 15 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
python-typeguard (4.4.4-1) unstable; urgency=medium

* Team upload.
* New upstream release.

-- Colin Watson <[email protected]> Mon, 18 Aug 2025 15:37:57 +0100

python-typeguard (4.4.2-1) unstable; urgency=medium

* Team upload.
* New upstream release.
- Switched to JSON output when running mypy (closes: #1098615).

-- Colin Watson <[email protected]> Mon, 24 Feb 2025 01:03:04 +0000

python-typeguard (4.4.1-1) unstable; urgency=medium

* Team upload.
Expand Down
2 changes: 1 addition & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Build-Depends:
mypy,
python3-setuptools,
python3-setuptools-scm,
python3-typing-extensions (>= 4.10.0),
python3-typing-extensions (>= 4.14.0),
Rules-Requires-Root: no
Standards-Version: 4.7.0
Homepage: https://github.com/agronholm/typeguard
Expand Down
36 changes: 31 additions & 5 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ Version history
This library adheres to
`Semantic Versioning 2.0 <https://semver.org/#semantic-versioning-200>`_.

**4.4.4** (2026-06-18)

- Fixed ``IndexError`` when using ``@typechecked`` on more than one function with the
same name under certain circumstances
(`#527 <https://github.com/agronholm/typeguard/issues/527>`_)
- Fixed ``TypeError`` during type checking when the value to check is a parametrized
generic class
(`#526 <https://github.com/agronholm/typeguard/issues/526>`_)

**4.4.3** (2025-06-05)

- Fixed ``@typechecked`` unable to find the target function or method if it or the
containing class had PEP 695 type parameters on them
(`#500 <https://github.com/agronholm/typeguard/issues/500>`_)
- Fixed handling of union types on Python 3.14
(`#522 <https://github.com/agronholm/typeguard/issues/522>`_)
- Fixed ``__type_params__`` getting lost when a function is instrumented

**4.4.2** (2025-02-16)

- Fixed ``TypeCheckError`` in unpacking assignment involving properties of a parameter
of the function (`#506 <https://github.com/agronholm/typeguard/issues/506>`_;
regression introduced in v4.4.1)
- Fixed display of module name for forward references
(`#492 <https://github.com/agronholm/typeguard/pull/492>`_; PR by @JelleZijlstra)
- Fixed ``TypeError`` when using an assignment expression
(`#510 <https://github.com/agronholm/typeguard/issues/510>`_; PR by @JohannesK71083)
- Fixed ``ValueError: no signature found for builtin`` when checking against a protocol
and a matching attribute in the subject is a built-in function
(`#504 <https://github.com/agronholm/typeguard/issues/504>`_)

**4.4.1** (2024-11-03)

- Dropped Python 3.8 support
Expand All @@ -22,18 +53,13 @@ This library adheres to
- Fixed checks against annotations wrapped in ``NotRequired`` not being run unless the
``NotRequired`` is a forward reference
(`#454 <https://github.com/agronholm/typeguard/issues/454>`_)
- Fixed the ``pytest_ignore_collect`` hook in the pytest plugin blocking default pytest
collection ignoring behavior by returning ``None`` instead of ``False``
(PR by @mgorny)

**4.4.0** (2024-10-27)

- Added proper checking for method signatures in protocol checks
(`#465 <https://github.com/agronholm/typeguard/pull/465>`_)
- Fixed basic support for intersection protocols
(`#490 <https://github.com/agronholm/typeguard/pull/490>`_; PR by @antonagestam)
- Fixed protocol checks running against the class of an instance and not the instance
itself (this produced wrong results for non-method member checks)

**4.3.0** (2024-05-27)

Expand Down
23 changes: 12 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
requires = [
"setuptools >= 64",
"setuptools >= 77",
"setuptools_scm[toml] >= 6.4"
]
build-backend = "setuptools.build_meta"
Expand All @@ -10,23 +10,23 @@ name = "typeguard"
description = "Run-time type checker for Python"
readme = "README.rst"
authors = [{name = "Alex Grönholm", email = "[email protected]"}]
license = {text = "MIT"}
license = "MIT"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
requires-python = ">= 3.9"
dependencies = [
"importlib_metadata >= 3.6; python_version < '3.10'",
"typing_extensions >= 4.10.0",
"typing_extensions >= 4.14.0",
]
dynamic = ["version"]

Expand All @@ -36,7 +36,10 @@ Documentation = "https://typeguard.readthedocs.io/en/latest/"
"Source code" = "https://github.com/agronholm/typeguard"
"Issue tracker" = "https://github.com/agronholm/typeguard/issues"

[project.optional-dependencies]
[project.entry-points]
pytest11 = {typeguard = "typeguard._pytest_plugin"}

[dependency-groups]
test = [
"coverage[toml] >= 7",
"pytest >= 7",
Expand All @@ -49,9 +52,6 @@ doc = [
"sphinx-rtd-theme >= 1.3.0",
]

[project.entry-points]
pytest11 = {typeguard = "typeguard._pytest_plugin"}

[tool.setuptools.package-data]
typeguard = ["py.typed"]

Expand Down Expand Up @@ -99,15 +99,16 @@ strict = true
pretty = true

[tool.tox]
env_list = ["py39", "py310", "py311", "py312", "py313"]
env_list = ["py39", "py310", "py311", "py312", "py313", "py314"]
skip_missing_interpreters = true
requires = ["tox >= 4.22"]

[tool.tox.env_run_base]
commands = [["coverage", "run", "-m", "pytest", { replace = "posargs", extend = true }]]
package = "editable"
extras = ["test"]
dependency_groups = ["test"]

[tool.tox.env.docs]
depends = []
extras = ["doc"]
dependency_groups = ["doc"]
commands = [["sphinx-build", "-W", "-n", "docs", "build/sphinx"]]
30 changes: 22 additions & 8 deletions src/typeguard/_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def check_callable(
if unfulfilled_kwonlyargs:
raise TypeCheckError(
f"has mandatory keyword-only arguments in its declaration: "
f'{", ".join(unfulfilled_kwonlyargs)}'
f"{', '.join(unfulfilled_kwonlyargs)}"
)

num_positional_args = num_mandatory_pos_args = 0
Expand Down Expand Up @@ -500,8 +500,17 @@ def check_class(
)
finally:
del errors # avoid creating ref cycle
elif not issubclass(value, expected_class): # type: ignore[arg-type]
raise TypeCheckError(f"is not a subclass of {qualified_name(expected_class)}")
else:
if isinstance(expected_class, generic_alias_types):
expected_class = get_origin(expected_class)

if isinstance(value, generic_alias_types):
value = get_origin(value)

if not issubclass(value, expected_class):
raise TypeCheckError(
f"is not a subclass of {qualified_name(expected_class)}"
)


def check_newtype(
Expand Down Expand Up @@ -533,7 +542,7 @@ def check_typevar(
) -> None:
if origin_type.__bound__ is not None:
annotation = (
Type[origin_type.__bound__] if subclass_check else origin_type.__bound__
type[origin_type.__bound__] if subclass_check else origin_type.__bound__
)
check_type_internal(value, annotation, memo)
elif origin_type.__constraints__:
Expand All @@ -550,7 +559,7 @@ def check_typevar(
get_type_name(constraint) for constraint in origin_type.__constraints__
)
raise TypeCheckError(
f"does not match any of the constraints " f"({formatted_constraints})"
f"does not match any of the constraints ({formatted_constraints})"
)


Expand Down Expand Up @@ -648,7 +657,12 @@ def check_io(


def check_signature_compatible(subject: type, protocol: type, attrname: str) -> None:
subject_sig = inspect.signature(getattr(subject, attrname))
subject_attr = getattr(subject, attrname)
try:
subject_sig = inspect.signature(subject_attr)
except ValueError:
return # this can happen with builtins where the signature cannot be retrieved

protocol_sig = inspect.signature(getattr(protocol, attrname))
protocol_type: typing.Literal["instance", "class", "static"] = "instance"
subject_type: typing.Literal["instance", "class", "static"] = "instance"
Expand Down Expand Up @@ -1036,7 +1050,7 @@ def builtin_checker_lookup(
and getattr(origin_type, "__qualname__", "").startswith("NewType.")
and hasattr(origin_type, "__supertype__")
):
# typing.NewType on Python 3.9 and below
# typing.NewType on Python 3.9
return check_newtype

return None
Expand All @@ -1061,7 +1075,7 @@ def load_plugins() -> None:
plugin = ep.load()
except Exception as exc:
warnings.warn(
f"Failed to load plugin {ep.name!r}: " f"{qualified_name(exc)}: {exc}",
f"Failed to load plugin {ep.name!r}: {qualified_name(exc)}: {exc}",
stacklevel=2,
)
continue
Expand Down
2 changes: 0 additions & 2 deletions src/typeguard/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ class TypeCheckConfiguration:
If set to ``True``, the code of modules or functions instrumented by typeguard
is printed to ``sys.stderr`` after the instrumentation is done

Requires Python 3.9 or newer.

Default: ``False``
"""

Expand Down
27 changes: 18 additions & 9 deletions src/typeguard/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,19 @@ def make_cell(value: object) -> _Cell:
def find_target_function(
new_code: CodeType, target_path: Sequence[str], firstlineno: int
) -> CodeType | None:
target_name = target_path[0]
for const in new_code.co_consts:
if isinstance(const, CodeType):
if const.co_name == target_name:
new_path = (
target_path[1:] if const.co_name == target_path[0] else target_path
)
if not new_path:
if const.co_firstlineno == firstlineno:
return const
elif len(target_path) > 1:
target_code = find_target_function(
const, target_path[1:], firstlineno
)
if target_code:
return target_code

continue

if target_code := find_target_function(const, new_path, firstlineno):
return target_code

return None

Expand Down Expand Up @@ -117,10 +118,18 @@ def instrument(f: T_CallableOrType) -> FunctionType | str:
new_function.__module__ = f.__module__
new_function.__name__ = f.__name__
new_function.__qualname__ = f.__qualname__
new_function.__annotations__ = f.__annotations__
new_function.__doc__ = f.__doc__
new_function.__defaults__ = f.__defaults__
new_function.__kwdefaults__ = f.__kwdefaults__

if sys.version_info >= (3, 12):
new_function.__type_params__ = f.__type_params__

if sys.version_info >= (3, 14):
new_function.__annotate__ = f.__annotate__
else:
new_function.__annotations__ = f.__annotations__

return new_function


Expand Down
24 changes: 19 additions & 5 deletions src/typeguard/_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def visit_BinOp(self, node: BinOp) -> Any:

if isinstance(node.op, BitOr):
# If either branch of the BinOp has been transformed to `None`, it means
# that a type in the union was ignored, so the entire annotation should e
# that a type in the union was ignored, so the entire annotation should be
# ignored
if not hasattr(node, "left") or not hasattr(node, "right"):
return None
Expand All @@ -384,6 +384,7 @@ def visit_BinOp(self, node: BinOp) -> Any:
elif self._memo.name_matches(node.right, *anytype_names):
return node.right

# Turn union types to typing.Union constructs on Python 3.9
if sys.version_info < (3, 10):
union_name = self.transformer._get_import("typing", "Union")
return Subscript(
Expand Down Expand Up @@ -1073,8 +1074,9 @@ def visit_Assign(self, node: Assign) -> Any:

path.insert(0, exp.id)
name = prefix + ".".join(path)
annotation = self._memo.variable_annotations.get(exp.id)
if annotation:
if len(path) == 1 and (
annotation := self._memo.variable_annotations.get(exp.id)
):
annotations_.append((Constant(name), annotation))
check_required = True
else:
Expand Down Expand Up @@ -1137,8 +1139,20 @@ def visit_NamedExpr(self, node: NamedExpr) -> Any:
func_name,
[
node.value,
Constant(node.target.id),
annotation,
List(
[
List(
[
Tuple(
[Constant(node.target.id), annotation],
ctx=Load(),
)
],
ctx=Load(),
)
],
ctx=Load(),
),
self._memo.get_memo_name(),
],
[],
Expand Down
Loading
Loading