feat: Implement __replace__ method in BaseContainer for copy.replace(… · dry-python/returns@3b9768d · GitHub
Skip to content

Commit 3b9768d

Browse files
Adrian AcalaAdrian Acala
authored andcommitted
feat: Implement __replace__ method in BaseContainer for copy.replace() support
- Added the __replace__ magic method to BaseContainer, enabling the creation of modified copies of immutable containers in line with Python 3.13's copy.replace() functionality. - Updated documentation to include usage examples and clarify the behavior of the new method. - Added tests to ensure the correct functionality of the __replace__ method and its integration with the copy module. - Updated CHANGELOG to reflect this new feature and its implications for container usage. Closes #1920.
1 parent ac1bf89 commit 3b9768d

4 files changed

Lines changed: 371 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions

‎docs/pages/container.rst‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,126 @@ Well, nothing is **really** immutable in python, but you were warned.
372372
We also provide :class:`returns.primitives.types.Immutable` mixin
373373
that users can use to quickly make their classes immutable.
374374

375+
Creating Modified Copies of Containers
376+
--------------------------------------
377+
378+
While containers are immutable, sometimes you need to create a modified copy
379+
of a container with different inner values. Since Python 3.13, ``returns``
380+
containers support the ``copy.replace()`` function via the ``__replace__``
381+
magic method.
382+
383+
.. code:: python
384+
385+
>>> from returns.result import Success, Failure
386+
>>> import copy, sys
387+
>>>
388+
>>> # Only run this example on Python 3.13+
389+
>>> if sys.version_info >= (3, 13):
390+
... # Replace the inner value of a Success container
391+
... original = Success(1)
392+
... modified = copy.replace(original, _inner_value=2)
393+
... assert modified == Success(2)
394+
... assert original is not modified # Creates a new instance
395+
...
396+
... # Works with Failure too
397+
... error = Failure("original error")
398+
... new_error = copy.replace(error, _inner_value="new error message")
399+
... assert new_error == Failure("new error message")
400+
...
401+
... # No changes returns the original object (due to immutability)
402+
... assert copy.replace(original) is original
403+
... else:
404+
... # For Python versions before 3.13, the tests would be skipped
405+
... pass
406+
407+
.. note::
408+
The parameter name ``_inner_value`` is used because it directly maps to the
409+
internal attribute of the same name in ``BaseContainer``. In the ``__replace__``
410+
implementation, this parameter name is specifically recognized to create a new
411+
container instance with a modified inner value.
412+
413+
.. warning::
414+
While ``copy.replace()`` works at runtime, it has limitations with static
415+
type checking. If you replace an inner value with a value of a different
416+
type, type checkers won't automatically infer the new type:
417+
418+
.. code:: python
419+
420+
# Example that would work in Python 3.13+:
421+
# >>> num_container = Success(123)
422+
# >>> str_container = copy.replace(num_container, _inner_value="string")
423+
# >>> # Type checkers may still think this is Success[int] not Success[str]
424+
>>> # The above is skipped in doctest as copy.replace requires Python 3.13+
425+
426+
Using ``copy.replace()`` with Custom Containers
427+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
428+
429+
If you create your own container by extending ``BaseContainer``, it will automatically
430+
inherit the ``__replace__`` implementation for free. This means your custom containers
431+
will work with ``copy.replace()`` just like the built-in ones.
432+
433+
.. code:: python
434+
435+
>>> from returns.primitives.container import BaseContainer
436+
>>> from typing import TypeVar, Generic
437+
>>> import copy, sys # Requires Python 3.13+ for copy.replace
438+
439+
>>> T = TypeVar('T')
440+
>>> class MyBox(BaseContainer, Generic[T]):
441+
... """A custom container that wraps a value."""
442+
... def __init__(self, inner_value: T) -> None:
443+
... super().__init__(inner_value)
444+
...
445+
... def __eq__(self, other: object) -> bool:
446+
... if not isinstance(other, MyBox):
447+
... return False
448+
... return self._inner_value == other._inner_value
449+
...
450+
... def __repr__(self) -> str:
451+
... return f"MyBox({self._inner_value!r})"
452+
453+
>>> # Create a basic container
454+
>>> box = MyBox("hello")
455+
>>>
456+
>>> # Test works with copy.replace only on Python 3.13+
457+
>>> if sys.version_info >= (3, 13):
458+
... new_box = copy.replace(box, _inner_value="world")
459+
... assert new_box == MyBox("world")
460+
... assert box is not new_box
461+
... else:
462+
... # For Python versions before 3.13
463+
... pass
464+
465+
By inheriting from ``BaseContainer``, your custom container will automatically support:
466+
467+
1. The basic container operations like ``__eq__``, ``__hash__``, ``__repr__``
468+
2. Pickling via ``__getstate__`` and ``__setstate__``
469+
3. The ``copy.replace()`` functionality via ``__replace__``
470+
4. Immutability via the ``Immutable`` mixin
471+
472+
Before Python 3.13, you can use container-specific methods to create modified copies:
473+
474+
.. code:: python
475+
476+
>>> from returns.result import Success, Failure, Result
477+
>>> from typing import Any
478+
479+
>>> # For Success containers, we can use .map to transform the inner value
480+
>>> original = Success(1)
481+
>>> modified = original.map(lambda _: 2)
482+
>>> assert modified == Success(2)
483+
484+
>>> # For Failure containers, we can use .alt to transform the inner value
485+
>>> error = Failure("error")
486+
>>> new_error = error.alt(lambda _: "new error")
487+
>>> assert new_error == Failure("new error")
488+
489+
>>> # For general containers without knowing success/failure state:
490+
>>> def replace_inner_value(container: Result[Any, Any], new_value: Any) -> Result[Any, Any]:
491+
... """Create a new container with the same state but different inner value."""
492+
... if container.is_success():
493+
... return Success(new_value)
494+
... return Failure(new_value)
375495
376496
.. _type-safety:
377497

‎returns/primitives/container.py‎

Lines changed: 69 additions & 4 deletions

0 commit comments

Comments
 (0)