sain.convert

Protocols for conversions between types.

The protocols in this module provide a way to convert from one type to another type. Each trait serves a different purpose:

  • Implement the From trait for consuming value-to-value conversions
  • Implement the Into trait for consuming value-to-value conversions to types outside the current crate
  • The TryFrom and TryInto traits behave like From and Into, but should be implemented when the conversion can fail.
  • Implement the ToString trait for explicitly converting objects to string.
Example
@dataclass
class Id(From[UUID], Into[int]):
    id: int | float

    @classmethod
    def from_t(cls, value: UUID) -> Self:
        # Keep in mind, this stores a 128 bit <long> integer.
        return cls(int(value))

    def into(self) -> int:
        return int(self.id)

# Simply perform conversions.
from_uuid = Id.from_t(uuid4())
into_int = from_uuid.into()

For type conversions that may fail, two safe interfaces, TryInto and TryFrom exist which deal with that.

This is useful when you are doing a type conversion that may trivially succeed but may also need special handling.

@dataclass
class Message(Into[bytes], TryFrom[bytes, None]):
    content: str
    id: int

    def into(self) -> bytes:
        return json.dumps(self.__dict__).encode()

    @classmethod
    def try_from(cls, value: bytes) -> Result[Self, None]:
        try:
            payload = json.loads(value)
            return Ok(cls(content=payload['content'], id=payload['id']))
        except (json.decoder.JSONDecodeError, KeyError):
            # Its rare to see a JSONDecodeError raised, but usually
            # keys goes missing, which raises a KeyError.
            return Err(None)

message_bytes = b'{"content": "content", "id": 0}'

match Message.try_from(message_bytes):
    case Ok(message):
        print("Successful conversion", message)
    case Err(invalid_bytes):
        print("Invalid bytes:", invalid_bytes)

payload = Message(content='...', id=0)
assert payload.into() == message_bytes
  1# BSD 3-Clause License
  2#
  3# Copyright (c) 2022-Present, nxtlo
  4# All rights reserved.
  5#
  6# Redistribution and use in source and binary forms, with or without
  7# modification, are permitted provided that the following conditions are met:
  8#
  9# * Redistributions of source code must retain the above copyright notice, this
 10#   list of conditions and the following disclaimer.
 11#
 12# * Redistributions in binary form must reproduce the above copyright notice,
 13#   this list of conditions and the following disclaimer in the documentation
 14#   and/or other materials provided with the distribution.
 15#
 16# * Neither the name of the copyright holder nor the names of its
 17#   contributors may be used to endorse or promote products derived from
 18#   this software without specific prior written permission.
 19#
 20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 23# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 24# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 25# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 26# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 27# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 28# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 30
 31"""
 32Protocols for conversions between types.
 33
 34The protocols in this module provide a way to convert from one type to another type. Each trait serves a different purpose:
 35
 36* Implement the From trait for consuming value-to-value conversions
 37* Implement the Into trait for consuming value-to-value conversions to types outside the current crate
 38* The TryFrom and TryInto traits behave like From and Into, but should be implemented when the conversion can fail.
 39* Implement the `ToString` trait for explicitly converting objects to string.
 40
 41Example
 42--------
 43```py
 44@dataclass
 45class Id(From[UUID], Into[int]):
 46    id: int | float
 47
 48    @classmethod
 49    def from_t(cls, value: UUID) -> Self:
 50        # Keep in mind, this stores a 128 bit <long> integer.
 51        return cls(int(value))
 52
 53    def into(self) -> int:
 54        return int(self.id)
 55
 56# Simply perform conversions.
 57from_uuid = Id.from_t(uuid4())
 58into_int = from_uuid.into()
 59```
 60
 61For type conversions that may fail, two safe interfaces, `TryInto` and `TryFrom` exist which deal with that.
 62
 63This is useful when you are doing a type conversion that may trivially succeed but may also need special handling.
 64
 65```py
 66@dataclass
 67class Message(Into[bytes], TryFrom[bytes, None]):
 68    content: str
 69    id: int
 70
 71    def into(self) -> bytes:
 72        return json.dumps(self.__dict__).encode()
 73
 74    @classmethod
 75    def try_from(cls, value: bytes) -> Result[Self, None]:
 76        try:
 77            payload = json.loads(value)
 78            return Ok(cls(content=payload['content'], id=payload['id']))
 79        except (json.decoder.JSONDecodeError, KeyError):
 80            # Its rare to see a JSONDecodeError raised, but usually
 81            # keys goes missing, which raises a KeyError.
 82            return Err(None)
 83
 84message_bytes = b'{"content": "content", "id": 0}'
 85
 86match Message.try_from(message_bytes):
 87    case Ok(message):
 88        print("Successful conversion", message)
 89    case Err(invalid_bytes):
 90        print("Invalid bytes:", invalid_bytes)
 91
 92payload = Message(content='...', id=0)
 93assert payload.into() == message_bytes
 94```
 95"""
 96
 97from __future__ import annotations
 98
 99__slots__ = ("Into", "TryInto", "From", "TryFrom", "ToString")
100
101import typing
102
103if typing.TYPE_CHECKING:
104    from typing_extensions import Self
105
106    from sain import Result
107
108T = typing.TypeVar("T")
109T_co = typing.TypeVar("T_co", contravariant=True)
110T_cov = typing.TypeVar("T_cov", covariant=True)
111E = typing.TypeVar("E")
112
113
114@typing.runtime_checkable
115class From(typing.Protocol[T_co]):
116    """Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into.
117
118    As the Rust documentation says, One should always prefer implementing From over Into
119    because implementing From automatically provides one with an implementation of Into.
120
121    But there's no such thing in Python, as it's impossible to auto-impl `Into<T>` for all types
122    that impl `From<T>`.
123
124    So for the sake of simplicity, You should implement whichever interface you want deal with,
125    Or simply, implement both as the same time.
126
127    Example
128    -------
129    ```py
130    @dataclass
131    class Id(From[str]):
132        value: int
133
134        @classmethod
135        def from_t(cls, value: str) -> Self:
136            return cls(value=int(value))
137
138    ```
139    """
140
141    __slots__ = ()
142
143    @classmethod
144    def from_t(cls, value: T_co) -> Self:
145        """Perform the conversion."""
146        raise NotImplementedError
147
148
149@typing.runtime_checkable
150class TryFrom(typing.Protocol[T_co, E]):
151    """Simple and safe type conversions that may fail in a controlled way under some circumstances.
152    It is the reciprocal of `TryInto`.
153
154    It is useful to implement this when you know that the conversion may fail in some way.
155
156    Generic Implementations
157    -------------------
158    This interface takes two type arguments, and return `Result<Self, E>`
159
160    * `T`: Which's the first generic `T` is the type that's being converted from.
161    * `E`: If the conversion fails in a way, this is what will return as the error.
162    * `Self`: Which's the instance of the class that is being converted into.
163
164    Example
165    -------
166    ```py
167    @dataclass
168    class Id(TryFrom[str, str]):
169        value: int
170
171        @classmethod
172        def try_from(cls, value: str) -> Result[Self, str]:
173            if not value.isnumeric():
174                # NaN
175                return Err(f"Couldn't convert: {value} to self")
176            # otherwise convert it to an Id instance.
177            return Ok(value=cls(int(value)))
178    ```
179    """
180
181    __slots__ = ()
182
183    @classmethod
184    def try_from(cls, value: T_co) -> Result[Self, E]:
185        """Perform the conversion."""
186        raise NotImplementedError
187
188
189@typing.runtime_checkable
190class Into(typing.Protocol[T_cov]):
191    """Conversion from `self`, which may or may not be expensive.
192
193    Example
194    -------
195    ```py
196    @dataclass
197    class Id(Into[str]):
198        value: int
199
200        def into(self) -> str:
201            return str(self.value)
202    ```
203    """
204
205    __slots__ = ()
206
207    def into(self) -> T_cov:
208        """Perform the conversion."""
209        raise NotImplementedError
210
211
212@typing.runtime_checkable
213class TryInto(typing.Protocol[T, E]):
214    """An attempted conversion from `self`, which may or may not be expensive.
215
216    It is useful to implement this when you know that the conversion may fail in some way.
217
218    Generic Implementations
219    -------------------
220    This interface takes two type arguments, and return `Result<T, E>`
221
222    * `T`: The first generic `T` is the type that's being converted into.
223    * `E`: If the conversion fails in a way, this is what will return as the error.
224
225    Example
226    -------
227    ```py
228    @dataclass
229    class Id(TryInto[int, str]):
230        value: str
231
232        def try_into(self) -> Result[int, str]:
233            if not self.value.isnumeric():
234                return Err(f"{self.value} is not a number...")
235            return Ok(int(self.value))
236    ```
237    """
238
239    __slots__ = ()
240
241    def try_into(self) -> Result[T, E]:
242        """Perform the conversion."""
243        raise NotImplementedError
244
245
246@typing.runtime_checkable
247class ToString(typing.Protocol):
248    """A trait for explicitly converting a value to a `str`.
249
250    Example
251    -------
252    ```py
253    class Value[T: bytes](ToString):
254        buffer: T
255
256        def to_string(self) -> str:
257            return self.buffer.decode("utf-8")
258    ```
259    """
260
261    __slots__ = ()
262
263    def to_string(self) -> str:
264        """Converts the given value to a `str`.
265
266        Example
267        --------
268        ```py
269        i = 5  # assume `int` implements `ToString`
270        five = "5"
271        assert five == i.to_string()
272        ```
273        """
274        raise NotImplementedError
275
276    def __str__(self) -> str:
277        return self.to_string()
@typing.runtime_checkable
class From(typing.Protocol[-T_co]):
115@typing.runtime_checkable
116class From(typing.Protocol[T_co]):
117    """Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into.
118
119    As the Rust documentation says, One should always prefer implementing From over Into
120    because implementing From automatically provides one with an implementation of Into.
121
122    But there's no such thing in Python, as it's impossible to auto-impl `Into<T>` for all types
123    that impl `From<T>`.
124
125    So for the sake of simplicity, You should implement whichever interface you want deal with,
126    Or simply, implement both as the same time.
127
128    Example
129    -------
130    ```py
131    @dataclass
132    class Id(From[str]):
133        value: int
134
135        @classmethod
136        def from_t(cls, value: str) -> Self:
137            return cls(value=int(value))
138
139    ```
140    """
141
142    __slots__ = ()
143
144    @classmethod
145    def from_t(cls, value: T_co) -> Self:
146        """Perform the conversion."""
147        raise NotImplementedError

Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into.

As the Rust documentation says, One should always prefer implementing From over Into because implementing From automatically provides one with an implementation of Into.

But there's no such thing in Python, as it's impossible to auto-impl Into<T> for all types that impl From<T>.

So for the sake of simplicity, You should implement whichever interface you want deal with, Or simply, implement both as the same time.

Example
@dataclass
class Id(From[str]):
    value: int

    @classmethod
    def from_t(cls, value: str) -> Self:
        return cls(value=int(value))
From(*args, **kwargs)
1430def _no_init_or_replace_init(self, *args, **kwargs):
1431    cls = type(self)
1432
1433    if cls._is_protocol:
1434        raise TypeError('Protocols cannot be instantiated')
1435
1436    # Already using a custom `__init__`. No need to calculate correct
1437    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1438    if cls.__init__ is not _no_init_or_replace_init:
1439        return
1440
1441    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1442    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1443    # searches for a proper new `__init__` in the MRO. The new `__init__`
1444    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1445    # instantiation of the protocol subclass will thus use the new
1446    # `__init__` and no longer call `_no_init_or_replace_init`.
1447    for base in cls.__mro__:
1448        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1449        if init is not _no_init_or_replace_init:
1450            cls.__init__ = init
1451            break
1452    else:
1453        # should not happen
1454        cls.__init__ = object.__init__
1455
1456    cls.__init__(self, *args, **kwargs)
@classmethod
def from_t(cls, value: -T_co) -> typing_extensions.Self:
144    @classmethod
145    def from_t(cls, value: T_co) -> Self:
146        """Perform the conversion."""
147        raise NotImplementedError

Perform the conversion.

@typing.runtime_checkable
class TryFrom(typing.Protocol[-T_co, ~E]):
150@typing.runtime_checkable
151class TryFrom(typing.Protocol[T_co, E]):
152    """Simple and safe type conversions that may fail in a controlled way under some circumstances.
153    It is the reciprocal of `TryInto`.
154
155    It is useful to implement this when you know that the conversion may fail in some way.
156
157    Generic Implementations
158    -------------------
159    This interface takes two type arguments, and return `Result<Self, E>`
160
161    * `T`: Which's the first generic `T` is the type that's being converted from.
162    * `E`: If the conversion fails in a way, this is what will return as the error.
163    * `Self`: Which's the instance of the class that is being converted into.
164
165    Example
166    -------
167    ```py
168    @dataclass
169    class Id(TryFrom[str, str]):
170        value: int
171
172        @classmethod
173        def try_from(cls, value: str) -> Result[Self, str]:
174            if not value.isnumeric():
175                # NaN
176                return Err(f"Couldn't convert: {value} to self")
177            # otherwise convert it to an Id instance.
178            return Ok(value=cls(int(value)))
179    ```
180    """
181
182    __slots__ = ()
183
184    @classmethod
185    def try_from(cls, value: T_co) -> Result[Self, E]:
186        """Perform the conversion."""
187        raise NotImplementedError

Simple and safe type conversions that may fail in a controlled way under some circumstances. It is the reciprocal of TryInto.

It is useful to implement this when you know that the conversion may fail in some way.

Generic Implementations

This interface takes two type arguments, and return Result<Self, E>

  • T: Which's the first generic T is the type that's being converted from.
  • E: If the conversion fails in a way, this is what will return as the error.
  • Self: Which's the instance of the class that is being converted into.
Example
@dataclass
class Id(TryFrom[str, str]):
    value: int

    @classmethod
    def try_from(cls, value: str) -> Result[Self, str]:
        if not value.isnumeric():
            # NaN
            return Err(f"Couldn't convert: {value} to self")
        # otherwise convert it to an Id instance.
        return Ok(value=cls(int(value)))
TryFrom(*args, **kwargs)
1430def _no_init_or_replace_init(self, *args, **kwargs):
1431    cls = type(self)
1432
1433    if cls._is_protocol:
1434        raise TypeError('Protocols cannot be instantiated')
1435
1436    # Already using a custom `__init__`. No need to calculate correct
1437    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1438    if cls.__init__ is not _no_init_or_replace_init:
1439        return
1440
1441    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1442    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1443    # searches for a proper new `__init__` in the MRO. The new `__init__`
1444    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1445    # instantiation of the protocol subclass will thus use the new
1446    # `__init__` and no longer call `_no_init_or_replace_init`.
1447    for base in cls.__mro__:
1448        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1449        if init is not _no_init_or_replace_init:
1450            cls.__init__ = init
1451            break
1452    else:
1453        # should not happen
1454        cls.__init__ = object.__init__
1455
1456    cls.__init__(self, *args, **kwargs)
@classmethod
def try_from(cls, value: -T_co) -> 'Result[Self, E]':
184    @classmethod
185    def try_from(cls, value: T_co) -> Result[Self, E]:
186        """Perform the conversion."""
187        raise NotImplementedError

Perform the conversion.

@typing.runtime_checkable
class Into(typing.Protocol[+T_cov]):
190@typing.runtime_checkable
191class Into(typing.Protocol[T_cov]):
192    """Conversion from `self`, which may or may not be expensive.
193
194    Example
195    -------
196    ```py
197    @dataclass
198    class Id(Into[str]):
199        value: int
200
201        def into(self) -> str:
202            return str(self.value)
203    ```
204    """
205
206    __slots__ = ()
207
208    def into(self) -> T_cov:
209        """Perform the conversion."""
210        raise NotImplementedError

Conversion from self, which may or may not be expensive.

Example
@dataclass
class Id(Into[str]):
    value: int

    def into(self) -> str:
        return str(self.value)
Into(*args, **kwargs)
1430def _no_init_or_replace_init(self, *args, **kwargs):
1431    cls = type(self)
1432
1433    if cls._is_protocol:
1434        raise TypeError('Protocols cannot be instantiated')
1435
1436    # Already using a custom `__init__`. No need to calculate correct
1437    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1438    if cls.__init__ is not _no_init_or_replace_init:
1439        return
1440
1441    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1442    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1443    # searches for a proper new `__init__` in the MRO. The new `__init__`
1444    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1445    # instantiation of the protocol subclass will thus use the new
1446    # `__init__` and no longer call `_no_init_or_replace_init`.
1447    for base in cls.__mro__:
1448        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1449        if init is not _no_init_or_replace_init:
1450            cls.__init__ = init
1451            break
1452    else:
1453        # should not happen
1454        cls.__init__ = object.__init__
1455
1456    cls.__init__(self, *args, **kwargs)
def into(self) -> +T_cov:
208    def into(self) -> T_cov:
209        """Perform the conversion."""
210        raise NotImplementedError

Perform the conversion.

@typing.runtime_checkable
class TryInto(typing.Protocol[~T, ~E]):
213@typing.runtime_checkable
214class TryInto(typing.Protocol[T, E]):
215    """An attempted conversion from `self`, which may or may not be expensive.
216
217    It is useful to implement this when you know that the conversion may fail in some way.
218
219    Generic Implementations
220    -------------------
221    This interface takes two type arguments, and return `Result<T, E>`
222
223    * `T`: The first generic `T` is the type that's being converted into.
224    * `E`: If the conversion fails in a way, this is what will return as the error.
225
226    Example
227    -------
228    ```py
229    @dataclass
230    class Id(TryInto[int, str]):
231        value: str
232
233        def try_into(self) -> Result[int, str]:
234            if not self.value.isnumeric():
235                return Err(f"{self.value} is not a number...")
236            return Ok(int(self.value))
237    ```
238    """
239
240    __slots__ = ()
241
242    def try_into(self) -> Result[T, E]:
243        """Perform the conversion."""
244        raise NotImplementedError

An attempted conversion from self, which may or may not be expensive.

It is useful to implement this when you know that the conversion may fail in some way.

Generic Implementations

This interface takes two type arguments, and return Result<T, E>

  • T: The first generic T is the type that's being converted into.
  • E: If the conversion fails in a way, this is what will return as the error.
Example
@dataclass
class Id(TryInto[int, str]):
    value: str

    def try_into(self) -> Result[int, str]:
        if not self.value.isnumeric():
            return Err(f"{self.value} is not a number...")
        return Ok(int(self.value))
TryInto(*args, **kwargs)
1430def _no_init_or_replace_init(self, *args, **kwargs):
1431    cls = type(self)
1432
1433    if cls._is_protocol:
1434        raise TypeError('Protocols cannot be instantiated')
1435
1436    # Already using a custom `__init__`. No need to calculate correct
1437    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1438    if cls.__init__ is not _no_init_or_replace_init:
1439        return
1440
1441    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1442    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1443    # searches for a proper new `__init__` in the MRO. The new `__init__`
1444    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1445    # instantiation of the protocol subclass will thus use the new
1446    # `__init__` and no longer call `_no_init_or_replace_init`.
1447    for base in cls.__mro__:
1448        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1449        if init is not _no_init_or_replace_init:
1450            cls.__init__ = init
1451            break
1452    else:
1453        # should not happen
1454        cls.__init__ = object.__init__
1455
1456    cls.__init__(self, *args, **kwargs)
def try_into(self) -> 'Result[T, E]':
242    def try_into(self) -> Result[T, E]:
243        """Perform the conversion."""
244        raise NotImplementedError

Perform the conversion.

@typing.runtime_checkable
class ToString(typing.Protocol):
247@typing.runtime_checkable
248class ToString(typing.Protocol):
249    """A trait for explicitly converting a value to a `str`.
250
251    Example
252    -------
253    ```py
254    class Value[T: bytes](ToString):
255        buffer: T
256
257        def to_string(self) -> str:
258            return self.buffer.decode("utf-8")
259    ```
260    """
261
262    __slots__ = ()
263
264    def to_string(self) -> str:
265        """Converts the given value to a `str`.
266
267        Example
268        --------
269        ```py
270        i = 5  # assume `int` implements `ToString`
271        five = "5"
272        assert five == i.to_string()
273        ```
274        """
275        raise NotImplementedError
276
277    def __str__(self) -> str:
278        return self.to_string()

A trait for explicitly converting a value to a str.

Example
class Value[T: bytes](ToString):
    buffer: T

    def to_string(self) -> str:
        return self.buffer.decode("utf-8")
ToString(*args, **kwargs)
1430def _no_init_or_replace_init(self, *args, **kwargs):
1431    cls = type(self)
1432
1433    if cls._is_protocol:
1434        raise TypeError('Protocols cannot be instantiated')
1435
1436    # Already using a custom `__init__`. No need to calculate correct
1437    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1438    if cls.__init__ is not _no_init_or_replace_init:
1439        return
1440
1441    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1442    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1443    # searches for a proper new `__init__` in the MRO. The new `__init__`
1444    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1445    # instantiation of the protocol subclass will thus use the new
1446    # `__init__` and no longer call `_no_init_or_replace_init`.
1447    for base in cls.__mro__:
1448        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1449        if init is not _no_init_or_replace_init:
1450            cls.__init__ = init
1451            break
1452    else:
1453        # should not happen
1454        cls.__init__ = object.__init__
1455
1456    cls.__init__(self, *args, **kwargs)
def to_string(self) -> str:
264    def to_string(self) -> str:
265        """Converts the given value to a `str`.
266
267        Example
268        --------
269        ```py
270        i = 5  # assume `int` implements `ToString`
271        five = "5"
272        assert five == i.to_string()
273        ```
274        """
275        raise NotImplementedError

Converts the given value to a str.

Example
i = 5  # assume `int` implements `ToString`
five = "5"
assert five == i.to_string()