Extending Typeguard
===================

.. py:currentmodule:: typeguard

Adding new type checkers
------------------------

The range of types supported by Typeguard can be extended by writing a
**type checker lookup function** and one or more **type checker functions**. The former
will return one of the latter, or ``None`` if the given value does not match any of your
custom type checker functions.

The lookup function receives three arguments:

#. The origin type (the annotation with any arguments stripped from it)
#. The previously stripped out generic arguments, if any
#. Extra arguments from the :class:`~typing.Annotated` annotation, if any

For example, if the annotation was ``tuple``,, the lookup function would be called with
``tuple, (), ()``. If the type was parametrized, like ``tuple[str, int]``, it would be
called with ``tuple, (str, int), ()``. If the annotation was
``Annotated[tuple[str, int], "foo", "bar"]``, the arguments would instead be
``tuple, (str, int), ("foo", "bar")``.

The checker function receives four arguments:

#. The value to be type checked
#. The origin type
#. The generic arguments from the annotation (empty tuple when the annotation was not
   parametrized)
#. The memo object (:class:`~.TypeCheckMemo`)

There are a couple of things to take into account when writing a type checker:

#. If your type checker function needs to do further type checks (such as type checking
   items in a collection), you need to use :func:`~.check_type_internal` (and pass
   along ``memo`` to it)
#. If you're type checking collections, your checker function should respect the
   :attr:`~.TypeCheckConfiguration.collection_check_strategy` setting, available from
   :attr:`~.TypeCheckMemo.config`

.. versionchanged:: 4.0
    In Typeguard 4.0, checker functions **must** respect the settings in
    ``memo.config``, rather than the global configuration

The following example contains a lookup function and type checker for a custom class
(``MySpecialType``)::

    from __future__ import annotations
    from inspect import isclass
    from typing import Any

    from typeguard import TypeCheckError, TypeCheckerCallable, TypeCheckMemo


    class MySpecialType:
        pass


    def check_my_special_type(
        value: Any, origin_type: Any, args: tuple[Any, ...], memo: TypeCheckMemo
    ) -> None:
        if not isinstance(value, MySpecialType):
            raise TypeCheckError('is not my special type')


    def my_checker_lookup(
        origin_type: Any, args: tuple[Any, ...], extras: tuple[Any, ...]
    ) -> TypeCheckerCallable | None:
        if isclass(origin_type) and issubclass(origin_type, MySpecialType):
            return check_my_special_type

        return None

Registering your type checker lookup function with Typeguard
------------------------------------------------------------

Just writing a type checker lookup function doesn't do anything by itself. You'll have
to advertise your type checker lookup function to Typeguard somehow. There are two ways
to do that (pick just one):

#. Append to :data:`typeguard.checker_lookup_functions`
#. Add an `entry point`_ to your project in the ``typeguard.checker_lookup`` group

If you're packaging your project with standard packaging tools, it may be better to add
an entry point instead of registering it manually, because manual registration requires
the registration code to run first before the lookup function can work.

To manually register the type checker lookup function with Typeguard::

    from typeguard import checker_lookup_functions

    checker_lookup_functions.append(my_checker_lookup)

For adding entry points to your project packaging metadata, the exact method may vary
depending on your packaging tool of choice, but the standard way (supported at least by
recent versions of ``setuptools``) is to add this to ``pyproject.toml``:

.. code-block:: toml

    [project.entry-points]
    typeguard.checker_lookup = {myplugin = "myapp.my_plugin_module:my_checker_lookup"}

The configuration above assumes that the **globally unique** (within the
``typeguard.checker_lookup`` namespace) entry point name for your lookup function is
``myplugin``, it lives in the ``myapp.my_plugin_module`` and the name of the function
there is ``my_checker_lookup``.

.. note:: After modifying your project configuration, you may have to reinstall it in
    order for the entry point to become discoverable.

.. _entry point: https://docs.python.org/3/library/importlib.metadata.html#entry-points
