Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bool not cast to int when using union passthrough strategy #623

Closed
danielnelson opened this issue Jan 30, 2025 · 1 comment
Closed

Bool not cast to int when using union passthrough strategy #623

danielnelson opened this issue Jan 30, 2025 · 1 comment

Comments

@danielnelson
Copy link

When structuring a bool as an int, the behavior is to convert it to an int:

converter = cattrs.Converter()
result = converter.structure(True, int)
assert type(result) is int
assert result == 1

But when using configure_union_passthrough structuring to a union, it doesn't get converted and remains a bool:

converter = cattrs.Converter()
cattrs.strategies.configure_union_passthrough(Union[int, bool, float, str, None], converter)
result = converter.structure(True, Union[int, float])
assert type(result) is bool
assert result

My expectation was that it would be converted to an int in both cases.

This is with cattrs 24.1.2.

@Tinche
Copy link
Member

Tinche commented Feb 1, 2025

Ooph, interesting.

Here's the thing: in Python, bool is a subclass of int. This isn't even a wart in the type system, it's a wart in the runtime itself.

>>> issubclass(bool, int)
True
>>> isinstance(True, int)
True
>>> True == 1
True

Now, when you just do converter.structure(True, int), cattrs will do coercion - it will try to turn True into an int by essentially doing int(True) for you.

The union passthrough strategy doesn't do coercion, but validation. This is by design - doing coercion here would introduce other design limitations which I want to avoid. In your case, it will essentially do: True.__class__ in {int, bool, float} and then return it. bool is in that set since there's logic in cattrs to include subclasses, and technically speaking, this is correct behavior (True is an int, from Python's perspective).

So I'm going to close the issue as WONTFIX since I'm not sure how to approach this in a general way.

That said, we can continue the conversation for your particular case if you like. If it's important to you to coerce True into an int here, you might do something like this:

from typing import Union

import cattrs
import cattrs.strategies

converter = cattrs.Converter()
cattrs.strategies.configure_union_passthrough(
    Union[int, bool, float, str, None], converter
)


_base_hook = converter.get_structure_hook(Union[int, float])


def coerce_to_int(val, _) -> Union[int, float]:
    res = _base_hook(val, _)
    if isinstance(res, int):
        res = int(res)
    return res


converter.register_structure_hook_func(lambda t: t == Union[int, float], coerce_to_int)


result = converter.structure(True, Union[int, float])

@Tinche Tinche closed this as not planned Won't fix, can't repro, duplicate, stale Feb 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants