6.10. Decorator Arguments

  • Used for passing extra configuration to decorators

  • Use more one level of nesting

>>> def mydecorator(a=1, b=2):
...     def decorator(func):
...         def wrapper(*args, **kwargs):
...             return func(*args, **kwargs)
...         return wrapper
...     return decorator
>>>
>>>
>>> @mydecorator(a=0)
... def myfunction():
...     ...
>>>
>>>
>>> myfunction()

6.10.1. Example

>>> def translate(lang='en'):
...     TRANSLATION = {
...         'Hello': {'en': 'Hello', 'pl': 'Cześć', 'ru': 'Привет'},
...         'Goodbye': {'en': 'Goodbye', 'pl': 'Pa', 'ru': 'Пока'},
...     }
...     def decorator(func):
...         def wrapper(*args, **kwargs):
...             result = func(*args, **kwargs)
...             i18n = TRANSLATION.get(result, result)
...             return i18n.get(lang, result) if type(i18n) else i18n
...         return wrapper
...     return decorator
>>>
>>>
>>> @translate(lang='en')
... def say_hello():
...     return 'Hello'
>>>
>>> say_hello()
'Hello'
>>>
>>>
>>> @translate(lang='pl')
... def say_hello():
...     return 'Hello'
>>>
>>> say_hello()
'Cześć'

6.10.2. Use Case - 1

>>> 
... @setup(...)
... @teardown(...)
... def test():
...     ...

6.10.3. Use Case - 2

  • Deprecated

>>> import warnings
>>>
>>>
>>> def deprecated(removed_in_version=None):
...     def decorator(func):
...         def wrapper(*args, **kwargs):
...             name = func.__name__
...             file = func.__code__.co_filename
...             line = func.__code__.co_firstlineno + 1
...             message = f'Call to deprecated function {name} in {file} at line {line}'
...             message += f'\nIt will be removed in {removed_in_version}'
...             warnings.warn(message, DeprecationWarning)
...             return func(*args, **kwargs)
...         return wrapper
...     return decorator
>>>
>>>
>>> @deprecated(removed_in_version=2.0)
... def myfunction():
...     pass
>>>
>>>
>>> myfunction()  
/home/python/myscript.py:11: DeprecationWarning: Call to deprecated function myfunction in /home/python/myscript.py at line 19
It will be removed in 2.0

6.10.4. Use Case - 3

  • Timeout (SIGALRM)

>>> from signal import signal, alarm, SIGALRM
>>> from time import sleep
>>>
>>>
>>> def timeout(seconds=1, error_message='Timeout'):
...     def on_timeout(signum, frame):
...         raise TimeoutError
...
...     def decorator(func):
...         def wrapper(*args, **kwargs):
...             signal(SIGALRM, on_timeout)
...             alarm(int(seconds))
...             try:
...                 return func(*args, **kwargs)
...             except TimeoutError:
...                 print(error_message)
...             finally:
...                 alarm(0)
...         return wrapper
...     return decorator
>>>
>>>
>>> @timeout(seconds=3)
... def countdown(n):
...     for i in reversed(range(n)):
...         print(i)
...         sleep(1)
...     print('countdown finished')
>>>
>>>
>>> countdown(10)  
9
8
7
Timeout

Note

Note to Windows users. Implementation of subprocess.Popen._wait()

>>> 
... def _wait(self, timeout):
...     """Internal implementation of wait() on Windows."""
...     if timeout is None:
...         timeout_millis = _winapi.INFINITE
...     else:
...         timeout_millis = int(timeout * 1000)
...     if self.returncode is None:
...         # API note: Returns immediately if timeout_millis == 0.
...         result = _winapi.WaitForSingleObject(self._handle,
...                                              timeout_millis)
...         if result == _winapi.WAIT_TIMEOUT:
...             raise TimeoutExpired(self.args, timeout)
...         self.returncode = _winapi.GetExitCodeProcess(self._handle)
...     return self.returncode

6.10.5. Use Case - 4

  • Timeout (Timer)

>>> from _thread import interrupt_main
>>> from threading import Timer
>>> from time import sleep
>>>
>>>
>>> def timeout(seconds=1.0, error_message='Timeout'):
...     def decorator(func):
...         def wrapper(*args, **kwargs):
...             timer = Timer(seconds, interrupt_main)
...             timer.start()
...             try:
...                 result = func(*args, **kwargs)
...             except KeyboardInterrupt:
...                 raise TimeoutError(error_message)
...             finally:
...                 timer.cancel()
...             return result
...         return wrapper
...     return decorator
>>>
>>>
>>> @timeout(seconds=3.0)
... def countdown(n):
...     for i in reversed(range(n)):
...         print(i)
...         sleep(1.0)
...     print('countdown finished')
>>>
>>>
>>> countdown(10)  
9
8
7
Traceback (most recent call last):
TimeoutError: Timeout

6.10.6. Use Case - 5

File settings.py:

>>> BASE_URL = 'https://python3.info'

File utils.py:

>>> from http import HTTPStatus
>>> import httpx
>>>
>>>
>>> def _request(url, method='GET'):
...     url = BASE_URL + url
...     resp = httpx.request(url, method)
...     if resp.staus_code != HTTPStatus.OK:
...         raise ConnectionError
...     return resp
>>>
>>>
>>> def get(url):
...     def decorator(func):
...         def wrapper():
...             resp = _request(url)
...             return func(resp.json())
...         return wrapper
...     return decorator

File main.py:

>>> @get('/users/')
... def get_users(data: list[dict] = None) -> list['User']:
...     ...
>>>
>>>
>>> users = get_users()  

6.10.7. Assignments

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Decorator Arguments Syntax
# - Difficulty: easy
# - Lines: 5
# - Minutes: 3

# %% English
# 1. Define decorator `result`
# 2. Decorator should take `a` and `b` as arguments
# 2. Define `wrapper` with `*args` and `**kwargs` parameters
# 3. Wrapper should call original function with its original parameters,
#    and return its value
# 4. Decorator should return `wrapper` function
# 5. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj dekorator `result`
# 2. Dekorator powinien przyjmować `a` i `b` jako argumenty
# 2. Zdefiniuj `wrapper` z parametrami `*args` i `**kwargs`
# 3. Wrapper powinien wywoływać oryginalną funkcję z jej oryginalnymi
#    parametrami i zwracać jej wartość
# 4. Decorator powinien zwracać funckję `wrapper`
# 5. Uruchom doctesty - wszystkie muszą się powieść

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isfunction

>>> assert isfunction(result), \
'Create result() function'

>>> assert isfunction(result(1, 2)), \
'result() should take two positional arguments'

>>> assert isfunction(result(a=1, b=2)), \
'result() should take two keyword arguments: a and b'

>>> assert isfunction(result(a=1, b=2)(lambda: ...)), \
'result() should return decorator which can take a function as arg'

>>> @result(a=1, b=2)
... def echo(text):
...     return text

>>> echo('hello')
'hello'
"""

# type: Callable[[int,int], Callable]
def result():
    ...


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Decorator Arguments Staff
# - Difficulty: easy
# - Lines: 4
# - Minutes: 3

# %% English
# 1. Create decorator `can_login`
# 2. To answer if person is staff check field:
#    `is_staff` in `users: list[dict]`
# 3. Decorator will call decorated function, only if all users
#    has field with specified value
# 4. Field name and value are given as keyword arguments to decorator
# 5. If user is not a staff:
#    raise `PermissionError` with message `USERNAME is not a staff`,
#    where USERNAME is user's username
# 6. Run doctests - all must succeed

# %% Polish
# 1. Stwórz dekorator `can_login`
# 2. Aby odpowiedzieć czy osoba jest staffem sprawdź pole:
#    `is_staff` in `users: list[dict]`
# 3. Dekorator wywoła dekorowaną funkcję, tylko gdy każdy członek
#    grupy ma pole o podanej wartości
# 4. Nazwa pola i wartość są podawane jako argumenty nazwane do dekoratora
# 5. Jeżeli użytkownik nie jest staffem:
#    podnieś `PermissionError` z komunikatem `USERNAME is not a staff`,
#    gdzie USERNAME to username użytkownika
# 6. Uruchom doctesty - wszystkie muszą się powieść

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isfunction

>>> assert isfunction(can_login), \
'Create can_login() function'

>>> assert isfunction(can_login('field', 'value')), \
'can_login() should take two positional arguments'

>>> assert isfunction(can_login(field='field', value='value')), \
'can_login() should take two keyword arguments: field and value'

>>> assert isfunction(can_login('field', 'value')(lambda: ...)), \
'can_login() should return decorator which can take a function'

>>> group1 = [
...     {'is_staff': True, 'username': 'mwatney'},
...     {'is_staff': True, 'username': 'mlewis'},
...     {'is_staff': True, 'username': 'rmartinez'},
... ]
...
>>> group2 = [
...     {'is_staff': False, 'username': 'avogel'},
...     {'is_staff': True,  'username': 'bjohanssen'},
...     {'is_staff': True,  'username': 'cbeck'},
... ]

>>> @can_login(field='is_staff', value=True)
... def login(users):
...    users = ', '.join(user['username'] for user in users)
...    return f'Logging-in: {users}'

>>> login(group1)
'Logging-in: mwatney, mlewis, rmartinez'

>>> login(group2)
Traceback (most recent call last):
PermissionError: avogel is not a staff

>>> @can_login(field='is_staff', value='yes')
... def login(users):
...    users = ', '.join(user['username'] for user in users)
...    return f'Logging-in: {users}'

>>> login(group1)
Traceback (most recent call last):
PermissionError: mwatney is not a staff

>>> login(group2)
Traceback (most recent call last):
PermissionError: avogel is not a staff
"""

# type: Callable[[str,str], Callable]
def can_login(field, value):
    def decorator(func):
        def wrapper(users):
            return func(users)
        return wrapper
    return decorator


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Decorator Arguments TypeCheck
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% English
# 1. Create decorator function `typecheck`
# 2. Decorator checks return type only if `check_return` is `True`
# 3. Run doctests - all must succeed

# %% Polish
# 1. Stwórz dekorator funkcję `typecheck`
# 2. Dekorator sprawdza typ zwracany tylko gdy `check_return` jest `True`
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - https://docs.python.org/3/howto/annotations.html
# - `inspect.get_annotations()`
# - `function.__code__.co_varnames`
# - `dict(zip(...))`
# - `dict.items()`
# - `dict1 | dict2` - merging dicts

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isfunction, isclass

>>> assert isfunction(typecheck), \
'Create typecheck() function'

>>> assert isfunction(typecheck(True)), \
'typecheck() should take one positional arguments'

>>> assert isfunction(typecheck(check_return=True)), \
'typecheck() should take one keyword arguments: check_return'

>>> assert isfunction(typecheck(check_return=True)(lambda: ...)), \
'typecheck() should return decorator which can take a function'

>>> @typecheck(check_return=True)
... def echo(a: str, b: int, c: float = 0.0) -> bool:
...     return bool(a * b)

>>> echo('one', 1)
True
>>> echo('one', 1, 1.1)
True
>>> echo('one', b=1)
True
>>> echo('one', 1, c=1.1)
True
>>> echo('one', b=1, c=1.1)
True
>>> echo(a='one', b=1, c=1.1)
True
>>> echo(c=1.1, b=1, a='one')
True
>>> echo(b=1, c=1.1, a='one')
True
>>> echo('one', c=1.1, b=1)
True
>>> echo(1, 1)
Traceback (most recent call last):
TypeError: "a" is <class 'int'>, but <class 'str'> was expected
>>> echo('one', 'two')
Traceback (most recent call last):
TypeError: "b" is <class 'str'>, but <class 'int'> was expected
>>> echo('one', 1, 'two')
Traceback (most recent call last):
TypeError: "c" is <class 'str'>, but <class 'float'> was expected
>>> echo(b='one', a='two')
Traceback (most recent call last):
TypeError: "b" is <class 'str'>, but <class 'int'> was expected
>>> echo('one', c=1.1, b=1.1)
Traceback (most recent call last):
TypeError: "b" is <class 'float'>, but <class 'int'> was expected

>>> @typecheck(check_return=True)
... def echo(a: str, b: int, c: float = 0.0) -> bool:
...     return str(a * b)
>>>
>>> echo('one', 1, 1.1)
Traceback (most recent call last):
TypeError: "return" is <class 'str'>, but <class 'bool'> was expected

>>> @typecheck(check_return=False)
... def echo(a: str, b: int, c: float = 0.0) -> bool:
...     return str(a * b)
>>>
>>> echo('one', 1, 1.1)
'one'
"""
from inspect import get_annotations


# type: Callable[[Callable], Callable]
def typecheck(func):
    annotations = get_annotations(func)

    def merge(args, kwargs) -> dict:
        args = dict(zip(annotations, args))
        return args | kwargs

    def check(argname, argvalue):
        argtype = type(argvalue)
        expected = annotations[argname]
        if argtype is not expected:
            raise TypeError(f'"{argname}" is {argtype}, '
                            f'but {expected} was expected')

    def wrapper(*args, **kwargs):
        arguments = merge(args, kwargs).items()
        for argname, argvalue in arguments:
            check(argname, argvalue)
        result = func(*args, **kwargs)
        check('return', result)
        return result

    return wrapper