Repository navigation
Complete type annotations #342
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
Closed
Closed
Changes from 1 commit
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
67909a0
Linearize base classes of SelectableGroups
andersk 70f1f24
DeprecatedList: Only warn when comparing to other lists
andersk cb0acdf
EntryPoints: Improve __getitem__ compatibility
andersk 0e960d4
SimplePath: Add .read_text(encoding) and .read_bytes()
andersk 53e1a53
FoldedCase: Improve split() compatibility
andersk 1790ef2
tests: Correct self.skip to self.skipTest
andersk 3e474b2
FoldedCase: Improve index() compatibility
andersk f21fe55
FoldedCase: Accept arbitrary objects in __eq__, __ne__, __contains__
andersk bb603e5
Replace always_iterable with ‘or []’
andersk f803c93
entry_points: Remove unnecessary functools.partial
andersk 6e3903e
EntryPoint: Remove None default for dist
andersk fb0adc7
tests: Replace __init__ with setUp.
andersk b1b358b
Complete type annotations
andersk 324fd33
mypy: Enable strict mode
andersk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Replace always_iterable with ‘or []’
always_iterable cannot be correctly annotated in Python’s type system. Signed-off-by: Anders Kaseorg <andersk@mit.edu>
- Loading branch information
commit bb603e5fd3fa7adb3382cf8f1d3302efafe98b68
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach would have been my instinct.
Since the docs for
.filessuggests to usealways_iterable, I'd prefer to follow that recommendation. I'll explore that option and then decide how to proceed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh. I see this change combines two changes (adding annotations and fixing the bug for #344). These two concerns should be separate. I recommend to remove these bugfixes from this PR so that it focuses on the type annotations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I already separated each functional change into separate atomic commits. They cannot be removed from this PR, because then it won’t pass mypy—the point of mypy is to find bugs like this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to remove
always_iterablein favor ofor []because the behavior ofalways_iterableis too dynamic to be correctly annotated in Python’s type system. The behavior where it converts a non-iterable input to a one-item iterator cannot be expressed; there’s anIterableconstraint but not a “non-iterable” constraint. Users who don’t need types are still free to usealways_iterable, of course.(Again, I’ve done this in a separate commit.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a shame and feels like a defect or weakness in the typing system. I'd like to see a solution that doesn't require changing the implementation. The
always_iterableidiom is a simple yet powerful abstraction and replaces with a single concise expression the common idiom of:If the type system can't support that level of dynamism, then the type system should be avoided until it can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that were all
always_iterabledid, it would be completely fine:(mypy playground)
But the current version has two extra features that interfere with static typing:
Firstly, a non-default
base_typemight change the behavior so thatobj: Optional[Iterable[T]]does not necessarily implyalways_iterable(obj): Iterator[T].Secondly, the
TypeErrorhandler means that one should also havealways_iterable(obj): Iterator[T]whenobj: T—but only ifTis not itself iterable. That extra non-iterable condition is not expressible in mypy, for the fundamental reason that it would break the Liskov substitution principle (e.g. every type is supposed to be a subtype ofobject, which is non-iterable). It’s also a fragile condition to rely on even when no annotations are involved (what ifTis updated in the future to become iterable?).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I had simplified the summary of
always_iterableto focus on the implementation for how it's used/recommended here. You're right that the implementation also addresses the concern of transforming a single item to an iterator over one item.It solves the common pattern where a function accepts both a single item or an iterable of items but intends to iterate over them. For example consider this change, where
always_iterabletransforms the branchy logic into a simple dict comprehension.That seems like a limitation of mypy. It seems to me that it should be possible to express any valid Python in mypy, especially something as simple as accepting an iterable or non-iterable. Is it possible the level of expressiveness you're trying to achieve is too great? I notice that
always_iterabledoes already have a type specification. Would that spec suffice or is it too generic for strict mode?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the same could be said for
or [], which is also fragile based on the value ofob.__bool__. If an object were to returnFalsefor__bool__but not implement iterable, it also would fail. I've found that for all (most?) practical cases, iterability is stable for the uses of any particular function, despite being unreliable in the general case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. But there’s a good reason for this limitation, since it’s a consequence of the more important Liskov substitution principle: wherever an expression of type
Tis expected, it should be type-correct to provide a value whose type is a subclass ofT.Remember, every non-iterable type might be subclassed by an iterable type (this is not an uncommon corner case: every iterable type is a subclass of
object). So if there were a hypothetical syntax that purported to guarantee thatalways_iterable(obj): Twhenobj: TandTis non-iterable, one could assign an instance of an iterable subclassUtoobj: Tand get back a result fromalways_iterable(obj)that breaks this guarantee.A simpler way to say this is that there’s no such thing as a non-iterable type, provided that one recognizes a type to implicitly include all of its subclasses. Giving up that provision would have unpleasant ripple effects on every other aspect of the type system (I think you would basically need to augment each type annotation with a flag saying whether or not a subclass is allowed).
Anyis never a type specification; it just disables type checking for that value and every value derived from it. (See the mypy documentation and avoid theAnytype.) Given that type checking is finding real bugs, we should be very hesitant to disable it for the sake of stylistic preferences.There will always be some cases where appeasing the type checker rather than disabling it will mean writing code in a different way than you might have otherwise. No static type system can be powerful enough to capture all the ways a language as dynamic as Python can be used. I acknowledge that this is unfortunate. But I hope you’ll find as I have that the benefits of full type checking outweigh these inconveniences.
It’s conventional in Python, to the point of being enshrined in PEP 8, that testing a sequence as a boolean is an idiomatic check for emptiness. But let’s consider all the corner cases:
or []is still empty.Nonebut truthy, sinceNoneTypecannot be subclassed.If that doesn’t satisfy you, here are some alternatives that type check:
Perhaps the separate
ifstatement is actually the clearest?