cloudflare-python icon indicating copy to clipboard operation
cloudflare-python copied to clipboard

.to_dict() returns object

Open Dreamsorcerer opened this issue 9 months ago • 6 comments

Confirm this is a Python library issue and not an underlying Cloudflare API issue.

  • [x] This is an issue with the Python library

Describe the bug

.to_dict() returns dict[str, object]: https://github.com/cloudflare/cloudflare-python/blob/475b534aadd740ae087967be82040bf0b365f8b9/src/cloudflare/_models.py#L108

This obviously creates type errors any time a user tries to use any of the values. If possible, the class should be made generic to return a more accurate type. If not, then the type should be dict[str, Any] in order to not create a load of type errors in user's code.

Dreamsorcerer avatar May 22 '25 14:05 Dreamsorcerer

Thanks for the report, but this is the intended behavior. .to_dict() is meant to be used to get a serialized/dumped version of the model, which is inherently not type safe.

If you want type safety, then I would recommend sticking with the Pydantic models. Let us know if there's a reason that doesn't work for you!

TomerAberbach avatar Jun 30 '25 22:06 TomerAberbach

To add to @TomerAberbach's response, if you don't care about type safety here then you can explicitly cast it to Any, for example

data: dict[str, Any] = response.to_dict()

RobertCraigie avatar Jun 30 '25 23:06 RobertCraigie

Thanks for the report, but this is the intended behavior. .to_dict() is meant to be used to get a serialized/dumped version of the model, which is inherently not type safe.

This is exactly what the purpose of Any is. object means the code is type safe with anything, which is obviously never going to be true. This is just going to create a lot of noise for users and require workarounds as above.

If I did want an extra layer of type safety, then I'd opt-in to it by enabling the any warnings in mypy.

Dreamsorcerer avatar Jun 30 '25 23:06 Dreamsorcerer

I'm also unclear how a serialized version of the model is inherently not type safe?

As mentioned, it should be possible to make it generic and return a TypedDict. I understand if you don't want to put the time into making that work, but I don't see any reason it couldn't be done.

Personally, I dislike Pydantic models and don't want another, slightly different typing system in my projects for people to learn. To that end, I always use Pydantic's TypeAdapter so I can stick to native Python types throughout my code (e.g. https://github.com/aio-libs/aiohttp-admin/blob/612475e372e7fa314a6af5aac5b69490df017e23/aiohttp_admin/security.py#L17-L25 / https://github.com/aio-libs/aiohttp-admin/blob/612475e372e7fa314a6af5aac5b69490df017e23/aiohttp_admin/views.py#L58).

Dreamsorcerer avatar Jul 01 '25 00:07 Dreamsorcerer

When we say type safety here, we're referring to the fact that the type checker will tell you that what you're doing isn't necessarily safe.

If we return Any then you can happily do unsafe things without realising they are unsafe - you shouldn't have to know to opt-in to particular type checker settings to get type safety.

Personally, I dislike Pydantic models and don't want another, slightly different typing system in my projects for people to learn.

How is the pydantic typing system any different than just declaring classes without it? It's just standard type annotations.

Additionally, from a consumers perspective, how are BaseModel instances any different than normal class instances? Especially if you're using pydantic's TypeAdapter as that means the types have to go through pydantic's typing system anyway? Or am I misunderstanding what you mean by typing system?

RobertCraigie avatar Jul 01 '25 02:07 RobertCraigie

If we return Any then you can happily do unsafe things without realising they are unsafe - you shouldn't have to know to opt-in to particular type checker settings to get type safety.

Well, the rest of the Python ecosystem seems to disagree. It's a tradeoff between type safety and usability, and strictness settings is how the Python ecosystem allows users to adjust the balance between them...

For example, json.loads() returns Any, not object: https://github.com/python/typeshed/blob/75d8c88ec5da2829f9e74b867998d058f454c3c7/stdlib/json/init.pyi#L49

You won't see object used anywhere in typeshed unless it is actually type safe (e.g. Any object can be safely passed to print(), but not all objects can be safely passed to json.dump(), so they use object and Any respectively). Users control how much type safety they want to enforce by adjusting the strictness settings.


Let's also look at future maintainability of the code.

The code currently returns a type which cannot be used for anything, rendering it useless. I therefore need to cast() or assign to a more generic type (as suggested above) in order to use it. By doing this, I am throwing away any type information provided.

If in future, you decide that it is worth implementing TypedDict or something else that improves typing accuracy, my code will still be throwing away all that type information and I won't get any type safety from these improvements.

Alternatively, if the code returned Any, like the rest of the Python ecosystem, my code would work without modification now. If I add any warnings for strictness (which I do), then I probably just have to ignore a single typing error. If the typing accuracy was improved later, I would automatically benefit from it and start seeing type errors in that code (and would be able to remove the ignore).

How is the pydantic typing system any different than just declaring classes without it? It's just standard type annotations.

Additionally, from a consumers perspective, how are BaseModel instances any different than normal class instances? Especially if you're using pydantic's TypeAdapter as that means the types have to go through pydantic's typing system anyway? Or am I misunderstanding what you mean by typing system?

There are a multitude of subtle differences that don't align. A BaseModel does not behave exactly the same as a dataclass, and certainly not the same as a TypedDict. It's another interface to learn, when the builtin types already provide everything I'd ever want.

TypeAdapter will always produce the expected result of the given type, bypassing any differences/quirks of a BaseModel, and can avoid ever exposing the BaseModel to a user, so they don't even need to know it exists, nevermind learn the new interface.

Dreamsorcerer avatar Jul 01 '25 13:07 Dreamsorcerer