This is kind of part of #214 but it stands for itself too.
Something like:
@attr.s(check_types=True)
class C:
x = attr.ib(type=int)
ought to work.
Regardless of checking the type, the type information has to be accessible somewhere so we can have first-class deserialization support.
+1
This would be awesome! Is the goal to enable type checking within attrs itself? and to support types as listed in typing.py? There's a few caveats that we've discovered, diving in.
1 is there's no standardized type checking behavior for collections in typing. From my understanding, this is still an area that needs more work:
https://github.com/python/typing/issues/136
Most people have been writing their own type validation libraries to compensate:
https://pypi.python.org/pypi/typeguard
All in all, I'm a huge fan of this direction. Just wondering what the choice is for type checker.
We don’t have concrete plans yet! Mostly because we want to make types & serialization first class concepts in attrs while doing this. :)
Wait, wrong ticket! :) Yeah, no still no idea yet!
I have a couple thoughts on this, if you're looking for ideas.
The type parameters should probably be equivalent to adding two behavior: validate and convert:
new_style = attr.ib(type=int)
old_style = attr.ib(validator=validators.instance_of(int), converter=int)
This works pretty well for primitive types. There's some nastiness with more complex types, which attrs probably shouldn't solve. For example, I'd expect to be able to do something like:
from datetime import daytime
@attr.s
class X:
date = attr.ib(type=datetime)
# epoch
x = X(date=1092183940129)
# ISO 8601 string
x = X(date="2017-06-30T14:14:57Z")
but these would fail, because the datetime constructor takes none of these.
I would argue probably just not handling this case, and letting people override with whatever complex converters they prefer:
attr.ib(type=datetime, converter=my_complex_converter)
The other part is using or supporting typing modules. this will require some choices in behavior (e.g. validating types recursively, or just validating the top level collection for collection types). I prefer recursive validation and conversion, personally.
Yes, feel free to dump all your thoughts. The more input we get beforehand, the less mistakes we’re likely to make. :)
OK, let's split the proposal into sub-features for easier commenting.
type parameter to attr.ib().@attr.s
class C:
x = attr.ib(type=int)
Presumably fields(C).x.type would be int then. I'm generally in favor of this. I already have a system like this in place for cattrs, except it uses metadata. I think this should be the backup though, for Pythons before 3.6. The 3.6 syntax reads much better for me.
@attr.s
class C:
x: int = attr.ib()
So basically like #214, but with the type argument for older Pythons.
My personal opinion is that static analysis is the future here. This means writing a MyPy plugin (which coincidentally I kinda promised to do, but I won't get around to until my main vacation at the end of July, probably), and maybe getting other vendors (flake8, pylint, pycharm...) in this space to cooperate too.
Static analysis has two major advantages. The first is that some types are impossible or exceedingly difficult to check for in runtime - think Iterator[int]. The other is performance. Generics in general are problematic.
@attr.s
class C:
x: List[int] = attr.ib()
inst = C(list(range(1000))) # Do what - iterate over the entire list?
inst.x.append('a string') # No one will complain in the runtime here.
Even if you have simple type annotations, without generics, the isinstance calls are kinda heavy. IIRC for ints it's actually less expensive to use a convert=int than validator=instance_of(int), even though they aren't exactly the same.
This isn't me getting into static vs dynamic typing, just stating the fact that static types are a tool with advantages over doing it in runtime. We're getting them in Python and it's exciting!
We will still need validators since validators can check the values of types, and MyPy can't. So you will always need a validator to ensure your int is positive, for example.
Now, as I mentioned, this is probably the future. We can't really rely on this today since MyPy doesn't even support the easy cases of attrs classes.
There is still value in runtime checking, of course. Not everyone will be able or willing to run MyPy over their code base, or maybe you want checks in the REPL, so us having a nice API for this would be good. This will require a little logic in our type validation, since not all types from typing can be used with isinstance.
The runtime type checks should be opt-in. I'm not sure if the opt-in should be on the class level (I feel like we'd need an opt-out at the attribute level) or at the attribute level, or both.
Let's experiment with some ideas for attribute-level opt-in.
from attr.validators import validate_type
def positive(_, __, val):
if val <= 0:
raise ValueError
@attr.s
class C:
x: int = attr.ib(validator=validate_type) # Magic! Requires additional support in attrs.
y: int = attr.ib(validator=[validate_type, positive])
z: int = attr.ib(validator=validate_type(positive))
I agree that at least, introducing the type attribute and doing nothing but attaching it to the field would be great. It's a fantastic building block to build off of.
It would also be great to fold in the type definition into the attr.ib object, if possible, so
x = attr.ib(type=int)
and
x: int = attr.ib()
are functionally equal (if both are declared, attr.ib(type=foo) could take precedence).
I know there's an existing PR around attrs support for MyPy, so that'll handle the static checks at some point.
Personally, I'm more interested in the dynamic checks. I'd like to integrate it as a serializer for an API validation library I've been working on (http://transmute-core.readthedocs.io/en/latest/), and attrs has a great philosophy with performance as a feature.
Regarding the way validation is declared, validate_type seems syntactically the nicest. Although the complexities of type validation is definitely a larger ordeal. A lot of choices as @Tinche points out. validate_type could do shallow or deep validation, and that's control you may want to leave to the user.
Providing simple integrations with existing runtime type checking / conversion tooling might be better. e.g. typeguard or cattrs.
I'm drinking Aperol Spritz by Lake Como right now but briefly:
More when I'm back after EP. Although I may sprint on it.
Turns out I didn’t and went to San Marino instead!
Anyhow, so far we seem to have a consensus that:
typex = attr.ib(type=int) if your life sucks and you can’t use Python 3.6x: int = attr.ib() if life is wonderful.check_type which will look at the type attribute and check.Correct?
So far that seems rather straightforward.
I’d just like to make 100% sure that it won’t paint us in a corner re: deserialization? I’d like explicit comments from the two who built deserialization on top of attrs: @glyph and @Tinche
I'm 100% on board with what you wrote FWIW.
FYI this is a very busy weekend for me but this is definitely in my queue to look at soon.
Cool, but don't overthink it. This is but a first building block. :)
👊 @glyph
(that’s a fist bump, not an attempt of intimidation thru violence :))
That (type attribute) seems like a solid first step.
@Tinche I'm thinking about your validate_type examples and I think it has legs.
But I'm wondering if it could also have global (ew?) switch of some sort. Since type validation, as you note, can be expensive, it's nice to do it statically if possible, but…
The runtime case I'd want to check is when unit testing, I think, just in case the static analysis toolchain is missing something. At least today, there are a lot of cases where mypy let's things slip, because it was hard to annotate something correctly so someone used Any, or you are getting a value from a library with no type hints or your code is only partly hinted, etc.
So I think there's a use case for: do validation when unit testing but not otherwise.
Perhaps I'm asking for a more fine-grained version of attr.set_run_validators…
Personally I'd rather have validation turned on all the time by default; I care more about correctness than speed. I want to rely on this to validate types when constructing them from wire formats. On PyPy, these isinstance checks will be no-opped out on the loops where they pass anyway, won't they?
@wsanchez fun fact: the reason behind attr.set_run_validators is precisely your use case. :) Because I want more checks at tests and less at runtime. attrs grew since its introduction, so things are more complicated now…
@glyph I agree on correctness > speed, however adding an implicit handbrake like this makes me uneasy. The big question is: has static type checking a future in Python or not? IOW: there will be a check_types in any case, but we’ll have to decide it’s default.
I personally tend to False, especially because we’ll most likely need to introduce a dependency on something like type guard, to do it properly.
@glyph I'm mostly in your camp, just trying to be sympathetic to the argument that sometimes, the validation is expensive overkill. That is, I might have a model object that, when I deserialize to it from JSON, I totally wan't validation, but when I read it from the database that I'm confident only have valid data, I might want to skip the validation, because I can test that pretty thoroughly.
I don't know what magic PyPy does to be smarter than me, and perhaps I should never optimize anything because PyPy will always do it better. Though I can't use PyPy yet, because at this point, I'm entered the "if it's not >=Py3.6, it's BULLSHIT" phase of my life. :-)
It's a good phase of life – enjoying not myself too.
In seriousness: unless someone comes up with a magical solution to validating types that goes beyond isinstance and doesn't introduce a new dependency, the default is settled.
I am using isinstance in my thing, but I'm not using the type directly. Specifically, here's a relevant function, which isn't complete:
def appropriate_type(T):
if isunion(T):
return tuple(appropriate_type(arg) for arg in T.__args__)
if issubclass(T, typing.List):
return list
if issubclass(T, typing.Dict):
return dict
if issubclass(T, typing.Set):
return set
return T
FWIW, the relevant function seems to be https://github.com/agronholm/typeguard/blob/master/typeguard.py#L348
Also the pkg is by @agronholm and I have a hunch he may be open to persuasion to donate a validator to us. :)
I’m still against adding a handbrake by default – it just feels wrong. :|
How would you feel if we added an alternate, non-type-checked classmethod constructor? Foo.raw for example?
a nocheck argument is a nice piece in general. great to just instantiate things as fast as possible if you're really confident the data is valid.
I'm +1 for no default type checking. It's a big change from the existing assumption that the validation is explicit, and I really like attrs because it's a nice syntax around creating classes, and keeps the classes lean.
If you don't want this type of type checking, don't pass type=?
@glyph I thought the idea was that type= is superfluous:
x: int = attribute() # Implies type=int
type could be useful for other things, like feeding data for serializers or extracting schema information from it.
maybe it is ok as a default...
$ python -m timeit 'isinstance("boo", str)'
10000000 loops, best of 3: 0.116 usec per loop
116 nanoseconds a check. Even if you checked 1000 objects with 10 attributes each, that only adds 1 millisecond per second of execution time.
Performance is even better, here:
★ pypy3 -m timeit 'isinstance("boo", str)'
WARNING: timeit is a very unreliable tool. use perf or something else for real measurements
pypy3 -m pip install perf
pypy3 -m perf timeit 'isinstance("boo", str)'
------------------------------------------------------------
1000000000 loops, average of 7: 0.000761 +- 1.25e-05 usec per loop (using standard deviation)
(CPython says 10000000 loops, best of 3: 0.0734 usec per loop)
@glyph I thought the idea was that type= is superfluous:
x: int = attribute() # Implies type=int
Sure; if you don't want it checked, don't pass the annotation (or say @attr.s(dont_run_validators=True)) or something.
Calling isinstance isn't the right validator implementation here, is it? That doesn't work for many of typing module types.
I can already use validator=instance_of. Making that automatic is nice and all, but "first class type support" is more than that:
x: Dict[str, Tuple[Foo, Bar]] = attribute() # Implies type=Dict[str, Tuple[Foo, Bar]]
Yeah, isinstance isn't 100% right, it's just the most common validator implementation.
I guess I can see why we might not want to do "deep" validation necessarily, especially of mutable types, given that they might be modified to not be type-compliant later.
Yes, as it has been said a few times now: type checking is gonna be about more than just isinstance. And as Wilfredo put it well: "first class support" is more than what we used to think about this kind of things. For me it's infrastructure to enable cool things – things we haven't thought of before. Type checking is just one of them and probably the most boring one.
It feels especially anachronistic to bake in runtime checks by default if you'd assume that static analysis/mypy become a generally usable thing.
In any case I think we have a general agreement about what to do. What's missing is the decision what to do by default. I suggest we discuss that once we have a type check implementation ready and can talk about performance implications more confidently.
But to me it still feel very antithetical to attrs' emphasis on performance and explicitness.
@glyph: I'd argue that interfaces are what matters, not the implementing class, so isinstance may be common, but it's certainly inferior. ("type" seems a misnomer in Python.)
I think the mutable object argument is a bit of a red herring, FWIW.
@hynek: I don't care what the default is; I think classes should be frozen by default, but I'm fine with adding frozen=True to all of my classes. Meh. I agree that not on by default seems safest, especially since I'm arguing for full typing support, which probably means a dependency on typeguard or similar.
Have we settled on the mechanics of specifying a type accepted by __init__ versus the ivar type? Is it this:
@attrs(check_types=True)
class C:
numbers: MutableSequence[int] = attribute(type=Sequence, convert=list)
foo: Deferred[Foo] = attribute(type=Awaitable[Foo], convert=ensureDeferred)
No, current state of affairs is that foo = attr.ib(type=X) is legacy syntax for foo: X = attr.ib()
Your examples should raise a conflict error I reckon. I understand what you’re driving at (cf. convert) but I can’t think of a good way to implement it. More likely maybe an additional attr.ib(init_type=Y).
Sorry, yes, right, type was the wrong name to suggest there; init_type is good.
Of course, more bad-ass would be looking at the type of the first arg to convert and using that. Mwahahaha. …must be bed time.
Just my 2 cents: Type checking of collections is quite brittle and potentially expensive, which is one reason why I recommend type checks to be disabled in production in typeguard's documentation. The abstract collection types in particular cannot be safely type checked. There are other subtle issues as well, like a misleading error message when you have an annotation like Union[List[int], str] and you pass it a list containing, say, a float.
Also, there is a new emerging type checker called pytypes. I've had a hand in that project as well and it has merged a few major features from typeguard already. I'm hoping that it will evolve eventually to the point where I can deprecate typeguard in its favor, but it's not there yet.
I think it would be great to get an immediate PR for annotating an attribute with a type, accessible from an instance via .type. IMHO small, focused PRs are a great way to keep the ball rolling, and everyone seems to be in agreement on the design of this necessary first step.
Once that is complete, various parties can get to work on the static and runtime typing checking tasks, and rally around more focused issue discussions. I'd be glad to help @Tinche with the mypy plugin (I've been messing around with some of my own).
That's certainly the plan.
Only open question to me is whether it makes any sense to artificially set __annotations__ if a user supplies .ib(type) in order to be backward compatible. Opinions @ambv?
Only open question to me is whether it makes any sense to artificially set __annotations__ if a user supplies .ib(type)
This is the first step, the second being copying from __annotations__ to Attribute.type? If so, I'd say no, if you want it in __annotations__, use a type annotation.
Please don't modify __annotations__. You will want to have real annotations present for static code analysis tools like mypy or PyCharm.
Please don't modify __annotations__. You will want to have real annotations present for static code analysis tools like mypy or PyCharm.
Modifying __annotations__ at runtime has no effect on static type analysis, which by definition never imports or runs your code. That said, my initial reaction is that I think __annotations__ should be copied to Attribute.type and not the other way around.
Modifying __annotations__ at runtime has no effect on static type analysis, which by definition never imports or runs your code. That said, my initial reaction is that I think __annotations__ should be copied to Attribute.type and not the other way around.
The examples I've seen so far would forego the annotations and instead pass the type off to attr.ib() and have that used for run-time type checking. Therefore I agree with you.
I just added a PR for the parts we all seem to agree on. Pop on by and let me know what you think.
Two more thoughts:
1) I like the suggestion from @Tinche for opt-in validation:
```python
from attr.validators import validate_type
@attr.s
class C:
x: int = attr.ib(validator=validate_type)
```
2) We can also automatically handle defaults to some degree. e.g.:
- `int` -> `int()`
- `str` -> `str()`
- `typing.List[str]` -> `Factory(list)` (the real class can be retrieved from `typing.List.__extra__`)
- `typing.Dict[str, int]` -> `Factory(dict)` (same as above)
A way of specifying this using something similar to 1 would be great:
```python
@attr.s
class C:
x: int = attr.ib(default=default_from_type)
```
3) Same goes for conversion. It should also be opt-in.
For convenience, we could combine all 3 in one go by providing a dict with each of the type-aware keyword arguments:
import attr
from attr import use_type
@attr.s
class C:
x: int = attr.ib(**use_type)
Sounds good to me.
FTR, the reason why I’m thinking about modifying __annotations__ is that IIUC, on Python 2, people are gonna have to write:
@attr.s
class C(object):
x = attr.ib(type=int) # type: int
Although I concur, that it should be probably solved in mypy.
The use_type magic goes a bit too far for attrs proper but we can revisit that topic once the groundwork is done.
FTR, the reason why I’m thinking about modifying
__annotations__is that IIUC, on Python 2, people are gonna have to write:@attr.s class C(object): x = attr.ib(type=int) # type: int
Static analysis and type annotations are pretty new, so before proceeding I should make a few non-obvious things clear so that we're all on the same page:
__annotations__ attribute is provided for runtime access to annotations:__annotations__ (the attribute is not present prior to python 3.4). __annotations__ are a direct corollary to (or bi-product of?) syntax support.__annotations__ attribute does not affect static type checking. thus modifying it at runtime would have no affect on mypy.Getting back to the original point: we can avoid the need for type comments in python 2.7 by providing pyi stubs with an overload specific to signatures that include the type argument. Here's a sketch:
from typing import Any, Mapping, Type, TypeVar, overload
T = TypeVar('T')
@overload
def attr(default: Any = ..., metadata: Mapping = ...) -> Any: ...
@overload
def attr(default: Any = ..., metadata: Mapping = ..., type: Type[T] = ...) -> T: ...
This says that if attr receives a type argument the function will return an instance of type. Otherwise, it returns Any (i.e. unknown).
That makes clear to static type-checkers that in your example x is an int without the need for type comments. So this is fine again:
@attr.s
class C(object):
x = attr.ib(type=int)
Hm, that actually looks quite clever!
I'll throw my 2c in here -- I've written a little PoC library a while ago that codegens runtime type checkers in an efficient fashion given the type annotations: http://github.com/aldanor/typo. It can handle most basic type classes, but also things like Tuple[T, Dict[U, T]] with type vars and bounds.
Compared to other similar libraries, specifying x: int literally results in if isinstance(x, int) at runtime and doesn't involve a gazillion closures and lookups and decorators around it, which for simple type checks like the latter is an important thing to have. TLDR: it's very fast.
It gets pretty complicated with generic type vars though (specifically, with union-type ones) since you have to generate some pretty complicated code to deal with generics inside sum-types (not going into further details but it's fairly hairy). If you ban generics inside sum-types then it becomes very simple and straightforward -- e.g. constraints like Tuple[T, Dict[U, T]] are handled quite easily.
Maybe some pieces could be reused or partially reused in here, saving everyone a bit of time :)
We have recently merged generic type support, maybe you'd like to play with it a bit and give feedback?
I'm don't think we'll rush adding the actual checking anytime soon. Maybe we'll even point at external solution, so feel free to experiment.
First class support is in thanks to #239, we can open new issues on what to do with those type now.
I was trying to find https://github.com/aldanor/typo today but instead I ran across https://github.com/RussBaz/enforce which looks like it may be a fairly full-featured version of the same thing.
There is also https://github.com/Stewori/pytypes
May the best project win.
On Tue, Nov 14, 2017 at 4:37 PM Glyph notifications@github.com wrote:
I was trying to find https://github.com/aldanor/typo today but instead I
ran across https://github.com/RussBaz/enforce which looks like it may be
a fairly full-featured version of the same thing.—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/python-attrs/attrs/issues/215#issuecomment-344447705,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAD3ExNEMEFKt-RbcCG4I_VwqdxFp-oEks5s2jI6gaJpZM4OFBOJ
.
First class support is in thanks to #239, we can open new issues on what to do with those type now.
Where is the discussion about runtime automatic type checking happening?
There is none so far. I believe a good first step would be to add generic class-wide validators (@attr.s(validator=whatever)) and then make type checking a special case of it, possibly with some syntactic sugar.
Thanks. Shall we open a separate issue or re-open this one?
I think a new one would be best.
Most helpful comment
First class support is in thanks to #239, we can open new issues on what to do with those type now.