docs/reference/typing-faq.md
This page answers some commonly asked questions about ty and Python's type system.
Check the documentation for the specific error code you are seeing; it may explain the problem.
Unknown type and when does it appear?Unknown is ty's way of representing a type that could not be fully inferred. It behaves the same
way as Any, but appears implicitly, rather than through an explicit Any annotation:
from missing_module import MissingClass # error: unresolved-import
reveal_type(MissingClass) # Unknown
ty also uses unions with Unknown to avoid false positive errors in untyped code while still
providing useful type information where possible.
For example, consider the following untyped Message class (which could come from a third-party
dependency that you have no control over). ty treats the data attribute as having type
Unknown | None, since there is no type annotation that restricts it further. The Unknown in the
union allows ty to avoid raising errors on the msg.data = … assignment. On the other hand, the
None in the union reflects the fact that data could possibly be None, and requires code that
uses msg.data to handle that case explicitly.
class Message:
data = None
def __init__(self, title):
self.title = title
def receive(msg: Message):
reveal_type(msg.data) # Unknown | None
msg = Message("Favorite color")
msg.data = {"color": "blue"}
(Full example in the playground)
int | float when I annotate something as float?The Python typing specification
includes a special rule for numeric types where an int can be used wherever a float is expected:
def circle_area(radius: float) -> float:
return 3.14 * radius * radius
circle_area(2) # OK: int is allowed where float is expected
This rule is a special case, since int is not actually a subclass of float. To support this, ty
treats float annotations as meaning int | float. Unlike some other type checkers, ty makes this
behavior explicit in type hints and error messages. For example, if you
hover over the radius parameter, ty
will show int | float.
A similar rule applies to complex, which is treated as int | float | complex.
!!! info
These special rules for `float` and `complex` exist for a reason. In almost all cases, you
probably want to accept both `int` and `float` when you annotate something as `float`.
If you really need to accept *only* `float` and not `int`, you can use ty's `JustFloat`
type. At the time of writing, this import needs to be guarded by a `TYPE_CHECKING` block:
```py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ty_extensions import JustFloat
else:
JustFloat = float
def only_actual_floats_allowed(f: JustFloat) -> None: ...
only_actual_floats_allowed(1.0) # OK
only_actual_floats_allowed(1) # error: invalid-argument-type
```
([Full example in the playground](https://play.ty.dev/fb034780-3ba7-4c6a-9449-5b0f44128bab))
If you need this for `complex`, you can use `ty_extensions.JustComplex` in a similar way.
list[Subtype] when a list[Supertype] is expected? { #invariant-generics }Let's say you have a class hierarchy with an Entry base class as well as Directory and File subclasses. Since
a Directory is an Entry, you can use it everywhere an Entry is expected.
You might therefore expect a list[Directory] to
be usable in any context where a list[Entry] is expected, but this is not
the case. The reason for this is mutability:
# Setup of `Entry`, `Directory`, and `File` classes (1)
def modify(entries: list[Entry]):
entries.append(File("README.txt")) # mutation
directories: list[Directory] = [Directory("Downloads"), Directory("Documents")]
modify(directories) # ty emits an error on this call
The full example might look like this:
from dataclasses import dataclass
@dataclass
class Entry:
path: str
def size_bytes(self) -> int: ...
@dataclass
class Directory(Entry):
def children(self) -> list[Entry]: ...
@dataclass
class File(Entry):
def content(self) -> bytes: ...
def modify(entries: list[Entry]):
entries.append(File("README.txt")) # mutation
directories: list[Directory] = [Directory("Downloads"), Directory("Documents")]
modify(directories) # ty emits an error on this call
You can try it out in this playground example.
The modify call mutates the contents of the directories list. After this call,
it contains two directories and one File, which clearly violates the
list[Directory] type annotation. If this call were allowed, subsequent code
that relies on the fact that directories only contains Directory instances might
break at runtime:
for directory in directories:
directory.children() # runtime: 'File' object has no attribute 'children'
!!! info
In type system terminology, we say `list` is *invariant*, which means that
just because `A` is a subtype of `B` does not mean that `list[A]` will be a
subtype of `list[B]`. The same is true for other builtin collections such as
`set` or `dict`. In contrast, read-only collections like `tuple` or
`frozenset` are *covariant* in their type parameter. It is safe to assign a
`frozenset[bool]` to a `frozenset[int]` because the contents cannot be
mutated.
You might run into problems with invariance in situations where mutability isn't required:
def total_size_bytes(entries: list[Entry]) -> int:
return sum(entry.size_bytes() for entry in entries)
# inferred as `list[Directory]`
media_entries = [Directory("Pictures"), Directory("Videos")]
# still a type-check error, but should be fine in principle (no mutation occurs)
size = total_size_bytes(media_entries)
To prevent this, you can adapt the signature of total_size_bytes to take an
argument of type
Sequence[Entry]
instead. This type describes read-only sequences (that contain values of type
Entry). Sequence is therefore covariant in its type parameter.
If you cannot adapt the signature of the function you are calling, you can also
widen the type of the argument by annotating media_entries as list[Entry].
In some cases it's also a reasonable solution to create a copy of the list
(total_size_bytes(list(media_entries))).
!!! note
If you are looking for a covariant alternative to `dict[str, V]`, you can use [`Mapping[str, V]`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping).
Callable has no attribute __name__?When you access __name__, __qualname__, __module__, or __doc__ on a value typed as Callable,
ty reports an unresolved-attribute error. This is because not all callables have these attributes.
Functions do (including lambdas), but other callable objects do not. The FileUpload class below, for
example, is callable, but instances of FileUpload do not have a __name__ attribute. Passing a
FileUpload instance to retry would lead to an AttributeError at runtime.
from typing import Callable
def retry(times: int, operation: Callable[[], bool]) -> bool:
for i in range(times):
# WRONG: `operation` does not necessarily have a `__name__` attribute
print(f"Calling {operation.__name__}, attempt {i + 1} of {times}")
if operation():
return True
return False
class FileUpload:
def __init__(self, name: str) -> None:
# …
def __call__(self) -> bool:
# …
retry(3, FileUpload("image.png"))
To fix this, you could use getattr with a fall back to a default name when the
attribute is not present (or use a hasattr(…, "__name__") check if you access
it multiple times):
name = getattr(operation, "__name__", "operation")
Alternatively, you could use an isinstance(…, types.FunctionType) check to narrow the type of
operation to something that definitely has a __name__ attribute:
if isinstance(operation, FunctionType):
print(f"Calling {operation.__name__}, attempt {i + 1} of {times}")
else:
print(f"Calling operation, attempt {i + 1} of {times}")
You can try various approaches in this playground example. See also this discussion for some plans to improve the developer experience around this in the future.
!!! info
ty has first-class support for intersection types. If you only want to accept function-like
callables, you could define `FunctionLikeCallable` as an intersection of `Callable` and
`types.FunctionType`:
```py
from typing import Callable, TYPE_CHECKING
from types import FunctionType
if TYPE_CHECKING:
from ty_extensions import Intersection
type FunctionLikeCallable[**P, R] = Intersection[Callable[P, R], FunctionType]
else:
FunctionLikeCallable = Callable
def retry(times: int, operation: FunctionLikeCallable[[], bool]) -> bool:
...
```
You can check out the full example [here](https://play.ty.dev/7a1ea4ab-04e1-4271-adf5-ddc3a5d2fcfd),
which demonstrates that `FileUpload` instances are no longer accepted by `retry`.
Top[list[Unknown]], and why does it appear?This type represents "all possible lists of any element type" (as opposed to list[Unknown], which
represents "a list of some unknown element type"). It usually arises from a check such as
if isinstance(x, list):. If x was previously of type Item | list[Item], you might expect this
check to narrow the type to list[Item], but ty respects the possibility that there could be a
common subclass of both Item and list (which may not be a list of Item!), and so the narrowed
type is instead (Item & Top[list[Unknown]]) | list[Item]. This code can be made more robust by
instead checking if instance(x, Item), or by declaring the Item type as @typing.final.
See also the discussion here and in this issue.
Not yet. A stricter inference mode is tracked in
this issue. In the meantime, you can consider using Ruff's
flake8-annotations rules to enforce
more explicit type annotations in your code.
ty does not report an error for unannotated function parameters, return types, or variables. When
ty encounters an unannotated symbol, it infers the type as Unknown
while still providing useful diagnostics where possible.
If you are looking for the equivalent of mypy's
disallow_untyped_defs
(error code: no-untyped-def), Ruff provides this as a set of opt-in lint rules via its
flake8-annotations (ANN) rule
group.
Some rules you might find useful include:
ANN001: Missing type
annotation for function argumentANN002: Missing type annotation for
*argsANN003: Missing type annotation for
**kwargsANN201:
Missing return type annotation for public functionANN202: Missing
return type annotation for private functionRUF045: Implicit class
variable in dataclassImport resolution issues are often caused by a missing or incorrect environment configuration. When ty reports "Cannot resolve imported module …", check the following:
Virtual environment: Make sure your virtual environment is discoverable. ty looks for an
active virtual environment via VIRTUAL_ENV or a .venv directory in your project root. See the
module discovery documentation for more details.
Project structure: If your source code is not in the project root or src/ directory,
configure environment.root in your pyproject.toml:
[tool.ty.environment]
root = ["./app"]
Third-party packages: Ensure dependencies are installed in your virtual environment. Run ty
with -v to see the search paths being used.
Compiled extensions: ty requires .py or .pyi files for type information. If a package
contains only compiled extensions (.so or .pyd files), you'll need stub files (.pyi) for ty
to understand the types. See also this issue which
tracks improvements in this area.
ty can work with monorepos, but automatic discovery of nested projects is limited. By default, ty
uses the current working directory or the --project option to determine the project root.
For monorepos with multiple Python packages, you have a few options:
Run ty per-package: Run ty check from each package directory, or use --project to specify
the package:
ty check --project packages/package-a
ty check --project packages/package-b
Configure multiple source roots: Use environment.root to specify
multiple source directories:
[tool.ty.environment]
root = ["packages/package-a", "packages/package-b"]
This has the disadvantage of treating all packages as a single project, which may lead to cases in which ty thinks something is importable when it wouldn't be at runtime.
You can follow this issue to get updates on this topic.
It depends on what you want to do. If you have a single inline-metadata script, you can type check
it with ty by using uv's --with-requirements flag to install the dependencies specified in the
script header:
uvx --with-requirements script.py ty check script.py
If you have multiple scripts in your workspace, ty does not yet recognize that they have different dependencies based on their inline metadata.
You can follow this issue for updates.
Not yet. You can track progress in this issue, which also includes some suggested manual hooks you can use in the meantime.
No. ty does not have a plugin system and there is currently no plan to add one.
We prefer extending the type system with well-specified features rather than relying on type-checker-specific plugins. That said, we are considering adding support for popular third-party libraries like pydantic, SQLAlchemy, attrs, or django directly into ty.