Immutable defaults for Python
Project description
immutable_defaults
Simple decorator to force immutability to function arguments by deepcopying. Never again pass None
when your heart wants to pass an empty list. Also works for arbitrary objects that can be deepcopied. Has simple config options for granularity or performance (copy vs deepcopy).
No dependencies.
In order to use various type hints we require Python >=3.12.
How to install
pip install immutable-defaults
or your equivalent (e.g. pdm add immutable-defaults
)
Example usage
from immutable_defaults import immutable_defaults
@immutable_defaults
def my_function(a: list = []):
a.append("world")
return a
print(my_function()) # ['world']
print(my_function(a=["hello"])) # ['hello', 'world']
print(my_function(["HELLO"])) # ['HELLO', 'world']
print(my_function()) # ['world']
@immutable_defaults(ignore=["b"])
def my_function2(a = ["hello"], b = []):
"""basic function with ignore parameter"""
a.append("world")
b.append("!")
return a + b
print(my_function2()) # ['hello', 'world', '!']
print(my_function2()) # ['hello', 'world', '!', '!']
print(my_function2()) # ['hello', 'world', '!', '!', '!']
# more exhaustive tests in tests/tests.py
Methods, Classmethods, and Staticmethods
The decorator works with methods, classmethods and staticmethods. Since @immutable_defaults
requires that the wrapped function is callable
, make sure that the outer decorator is @classmethod
/@staticmethod
.
Optional keyword arguments
-
@immutable_defaults
can be called with keyword argumentsdeepcopy
andignore
. -
deepcopy: boolean | Iterable[str] = True
- if
True
then defaults are copied withcopy.deepcopy
. If False, then withcopy.copy
. - If passed an iterable of argument names then those arguments will be deep copied and other mutable defaults will be shallow copied, e.g. in the below
a
andarg
will be deep copied whileb
will be shallow copied.
@immutable_defaults(deepcopy=["a","arg"]) def f(a=[[1]], b=[], arg={1: {2}}): ...
- if
-
ignore: Iterable[str] | None = None
- all argument names passed will have the default Python behavior.
Input validation
- We check that you cannot have the same mutable object (as per
a is b
comparison) marked for both shallow and deep copying. For example, the below will raise anImmutableDefaultsError
:
xss = [[1]]
@immutable_defaults(deepcopy=["xss2"]) # raises ImmutableDefaultsError
def f(x, xss1 = xss, xss2 = xss): ...
- Similarly, we check that you cannot ignore and not ignore the same mutable object. For example, the below will raise an
ImmutableDefaultsError
:
xss = [[1]]
@immutable_defaults(ignore=["xss2"]) # raises ImmutableDefaultsError
def f(x, xss1 = xss, xss2 = xss): ...
- A
KeyError
is raised if eitherdeepcopy
orignore
have arguments that cannot be found in the signature of the decorated function.- It would have been easy to silently do nothing when variables in
ignore
are not present, but this would make typos very hard to debug.
- It would have been easy to silently do nothing when variables in
ignore
takes precedence overdeepcopy
, i.e.@immutable_defaults(ignore=["x"], deepcopy=["x"])
will do the same thing as@immutable_defaults(ignore=["x"])
Prior art
(Comments valid May 13 2024)
- comparison with https://pypi.org/project/immutable_default_args/
- we deep copy all defaults except for standard immutable types (int, float, complex, bool, str, tuple, frozenset). This means we cover sets, and also other custom mutable objects that implement
__deepcopy__
(or optionally__copy__
). - Instead of a metaclass, we have a class decorator.
- we deep copy all defaults except for standard immutable types (int, float, complex, bool, str, tuple, frozenset). This means we cover sets, and also other custom mutable objects that implement
- comparison with https://pypi.org/project/python-immutable/
- comparison with https://github.com/roodrepo/default_mutable/
- we have typed code and IMO better naming.
- comparison with https://pypi.org/project/python-none-objects/
- completely different solution to the mutable default arguments problem.
- only works for empty containers.
Todo
- Performance benchmarking - what is the overhead?
- Make publishing to pypi part of github workflow
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for immutable_defaults-0.1.1-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4fec821e64cf5f21cdcda6dd4fee7f3c97480f59cffd8cf81883ff35402a062f |
|
MD5 | d8e1016051170250f1975d4e00bc07c0 |
|
BLAKE2b-256 | 35308098e7027ce0d7854dd929906c65487e4f3dfe620b9ce6b4f5b6e7fb5d23 |