Skip to content

Annotate SearchStrategy.filter with TypeGuard overload#4672

Open
CharString wants to merge 1 commit intoHypothesisWorks:masterfrom
CharString:annotate-filter
Open

Annotate SearchStrategy.filter with TypeGuard overload#4672
CharString wants to merge 1 commit intoHypothesisWorks:masterfrom
CharString:annotate-filter

Conversation

@CharString
Copy link
Copy Markdown
Contributor

TypeIs[T] case is not expressible (yet?), because you can't express the intersection type:

@overload
def filter(self, condition: Callable[[Ex], TypeIs[T]]) -> "SearchStrategy[Ex & T]": ...

TypeIs case is not expressible (yet?), because you can't express the
intersection type:

@overload
def filter(self, condition: Callable[[Ex], TypeIs[T]]) -> "SearchStrategy[Ex & T]": ...

Note, while mypy accepts this, I doubt the ListStrategy annotation of
`list[Ex]` is correct, because `Ex` is defined covariant and list is
invariant in its argument type.

class ListStrategy(SearchStrategy[list[Ex]]):
@Liam-DeVoe
Copy link
Copy Markdown
Member

I don't understand the motivation behind this change. TypeGuard is for use in narrowing different branches of a conditional, and I don't see how that gets us anything beyond the current annotations. Can you give an example of a before/after type check that is improved by this change?

@CharString
Copy link
Copy Markdown
Contributor Author

CharString commented Mar 10, 2026

@Liam-DeVoe A filter is a different branch of a conditional, in the same sense tail recursion is a for loop.

For some strategies Ex is very broad. e.g. hypothesis_jsonschema.from_schema: SearchStrategy[JSONType] a TypeGuard can narrow it down.

The semantics from set theoretic types (i.e. types are sets of values) are that a strategy samples from the Ex set and that filter defines a subset of Ex. And true, any sample from the filter is still also in Ex, but often that subset has a name too. TypeGuard allows us to express that name.

And in a world where a bunch of code is generated by agents, having these types explicit and readable in our PBTs, helps with raising trust in the code.

@Liam-DeVoe
Copy link
Copy Markdown
Member

Liam-DeVoe commented Mar 10, 2026

Here's an example of strategy whose inferred type changes:

from typing import TypeGuard

def is_str(x: object) -> TypeGuard[str]:
    return isinstance(x, str)

s = from_type(object).filter(is_str)
# previously: SearchStrategy[object]
# this PR: SearchStrategy[str]
reveal_type(s)

In the future it would help to give a motivating example in PRs like this!

Let's add some tests to revealed_types.py that use type guards in .filter.

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

Successfully merging this pull request may close these issues.

2 participants