This recently came up in a discussion. A lot of red-knot tests require some form of "setup" in the sense that they create variables of a particular type. This is not always straightforward. For example, to create a variable of type int, you need to make sure that it doesn't end up as a LiteralInt[…]. So some tests use a pattern like this:
def int_instance() -> int: ...
x = int_instance() # to create x: int
To create a variable with a union type, a lot of tests follow a pattern like
x = a if flag or b # to create x: A | B
It's unclear to me if this really requires any action, but I thought it might make sense to discuss this in a bit more detail. Here are some approaches (some less generic than others) that I could think of.
1. def f() -> MyDesiredType; x = f()
Upsides:
- You can specify
MyDesiredType directly
Downsides:
- Does not work for union types (yet).
- Rather verbose, requires coming up with yet another name (for the function)
- Does not work at runtime (i.e. you can't just paste this into an interpreter and see what the runtime type is).
2. def f(x: MyDesiredType): …
Upsides:
- You can specify
MyDesiredType directly
- Fewer lines than the pattern above
Downsides:
- Does not work yet
- All tests are now within a function, requires coming up with a name for the function
- Does not work at runtime (see above)
3. a if flag or b
(only relevant for union types)
Upsides:
- Does work at runtime, if you setup
flag beforehand
Downsides:
- Can't see the result type in code
- Relies on an undefined variable, which leads to extraneous diagnostic outputs (e.g. if you play with these snippets interactively, or if you want to see what other type checkers do). Maybe this could be prevented by injecting
flag into the test environment somehow.
- Arguably not very beginner-friendly (what is
flag?!)
4. Helper functions like one_of(a, b)
We could inject new functions, just for testing purposes. For example, we might have a function similar to
def one_of[A, B](a: A, b: B) -> A | B:
if random.randint(0, 1):
return x
else:
return y
to easily create union types
Upsides(?):
x = one_of(1, None) is slightly more readable than x = 1 if flag else None (but only if you know what one_of does)
- Works at runtime, but requires having the helper functions available
Downsides:
- Can't see the result type in code
- The snippets are not self-contained anymore. You can not simply copy a snippet and try it out in the mypy playground, for example (unless you have a copy of the test utilities in there already).
- Requires beginners to learn about these utilities
5. A magic conjure function
I'm not even sure if this is technically possible, but other languages have ways to create values of type T out of nothing. Not actually, of course. But for the purpose of doing interesting things at "type check time". For example, C++ has std::declval<T>(). Rust has let x: T = todo!(). Functional languages have absurd :: ⊥-> T.
You can't specify explicit generic parameters in a function call in Python (?), so we couldn't do something like x = conjure[int | None](), but maybe there is some way to create a construct conceptually similar to
def conjure[T]() -> T: ... # Python type checkers don't like this
I think I would personally prefer the simple def f(x: MyDesiredType): … approach, once we make that work.
This recently came up in a discussion. A lot of red-knot tests require some form of "setup" in the sense that they create variables of a particular type. This is not always straightforward. For example, to create a variable of type
int, you need to make sure that it doesn't end up as aLiteralInt[…]. So some tests use a pattern like this:To create a variable with a union type, a lot of tests follow a pattern like
It's unclear to me if this really requires any action, but I thought it might make sense to discuss this in a bit more detail. Here are some approaches (some less generic than others) that I could think of.
1.
def f() -> MyDesiredType; x = f()Upsides:
MyDesiredTypedirectlyDownsides:
2.
def f(x: MyDesiredType): …Upsides:
MyDesiredTypedirectlyDownsides:
3.
a if flag or b(only relevant for union types)
Upsides:
flagbeforehandDownsides:
flaginto the test environment somehow.flag?!)4. Helper functions like
one_of(a, b)We could inject new functions, just for testing purposes. For example, we might have a function similar to
to easily create union types
Upsides(?):
x = one_of(1, None)is slightly more readable thanx = 1 if flag else None(but only if you know whatone_ofdoes)Downsides:
5. A magic
conjurefunctionI'm not even sure if this is technically possible, but other languages have ways to create values of type
Tout of nothing. Not actually, of course. But for the purpose of doing interesting things at "type check time". For example, C++ hasstd::declval<T>(). Rust haslet x: T = todo!(). Functional languages haveabsurd :: ⊥-> T.You can't specify explicit generic parameters in a function call in Python (?), so we couldn't do something like
x = conjure[int | None](), but maybe there is some way to create a construct conceptually similar toI think I would personally prefer the simple
def f(x: MyDesiredType): …approach, once we make that work.