Pyright is a powerful, fast, and feature-packed static type checker explicitly designed for Python. It helps to ensure code quality, catch errors early, and boost productivity through static type checking.
This guide will walk you through how to set up and use Pyright in your Python projects.
Prerequisites
Before diving into Pyright, ensure you have the following prerequisites:
- A recent version of Python installed on your machine. This tutorial assumes you are using Python 3.13 or higher.
- Familiarity with basic Python programming concepts, including type hints and annotations
Step 1 — Setting up the project directory
In this section, you will create a new project directory, set up a virtual environment, and install Pyright within that environment.
First, create a new directory for your project and navigate into it:
mkdir pyright-demo && cd pyright-demo
Next, create and activate a virtual environment:
python3 -m venv venv
source venv/bin/activate
Pyright can be installed easily using pip
, the standard package manager for Python.
To install Pyright, run the following command in your terminal or command prompt:
pip install pyright
This will install Pyright as a Python package, making it available for static type checking in your projects.
After installation, verify that Pyright is correctly installed by running:
pyright --version
If the installation is successful, this command will display the installed version of Pyright:
pyright 1.1.394
Now that Pyright is installed let's continue configuring it for your project.
Step 2 — Getting started with Pyright
Now that Pyright is installed, it's time to configure it for your project. In this step, you will create a configuration file to customize Pyright’s behavior and ensure it works optimally within your project.
Pyright uses a configuration file named pyrightconfig.json
to define settings such as Python version, type-checking rules, and excluded directories.
Create the pyrightconfig.json
in a text editor in your project root and add the following default configuration:
{
"include": ["src"],
"exclude": ["venv"],
"pythonVersion": "3.13",
"typeCheckingMode": "basic"
}
The "include"
option directs Pyright to check files in src
, while "exclude"
prevents it from analyzing venv
.
"pythonVersion"
sets the Python version for type checking, and "typeCheckingMode": "basic"
provides moderate checks, which can be set to "strict"
for stricter enforcement.
To test Pyright, create a main.py
Python script inside a src
directory and add the following code:
def greet(name: str) -> str:
return "Hello, " + name
print(greet("Pyright"))
print(greet(42)) # This should trigger a type error
Now, run Pyright to check for type errors in your project:
pyright
If everything is set up correctly, Pyright should display an error for the second print(greet(42))
line, as it passes an integer instead of a string:
...pyright-demo/src/main.py:6:13 - error: Argument of type "Literal[42]" cannot be assigned to parameter "name" of type "str" in function "greet"
"Literal[42]" is not assignable to "str" (reportArgumentType)
1 error, 0 warnings, 0 information
This confirms that Pyright is working correctly and catching type errors in your code. The error message clearly indicates that you're trying to pass a number (42) to a function that expects a string parameter.
This is precisely the kind of type of safety that Pyright provides, helping you catch potential bugs before they make it to production.
You can also run Pyright in watch mode to automatically check for errors as you make changes to your code:
pyright --watch
Now that Pyright is configured and running, the next step is to explore how to use type hints effectively to improve your code quality and catch errors early.
Step 3 — Understanding Pyright’s type checking
Now that Pyright is installed and configured, it’s important to understand how it performs type checking in your Python code.
Pyright uses static analysis to check types without running your Python program. It detects mismatches between declared types and actual usage, ensuring type correctness throughout your codebase.
For example, consider the following function:
def add_numbers(a: int, b: int) -> int:
return a + b
print(add_numbers(5, 10)) # Correct usage
print(add_numbers("5", 10)) # Incorrect usage, will trigger an error
When pyright
runs on this file will catch the type mismatch:
src/main.py:6:19 - error: Argument of type "Literal['5']" cannot be assigned to parameter "a" of type "int" in function "add_numbers"
"Literal['5']" is not assignable to "int" (reportArgumentType)
1 error, 0 warnings, 0 informations
Pyright correctly identifies that a string ("5"
) cannot be used where an integer is expected, preventing a potential runtime error.
Type checking modes: basic vs. strict
Pyright provides two levels of type checking: "basic" and "strict". You can configure these in your pyrightconfig.json
file.
The basic mode is the default setting and provides moderate type checking. It allows:
- Untyped functions
- Implicit type inference
- Some relaxed type rules
For example, the following function won't trigger an error in basic mode:
def greet(name):
return "Hello, " + name
Even though name
lacks a type hint, Pyright assumes it could be any type.
0 errors, 0 warnings, 0 informations
Watching for file changes...
However, the strict mode enforces stronger type-checking and requires all functions to have explicit type annotations. It will:
- Require type annotations for function parameters and return values
- Detect untyped variables
- Prevent implicit type conversions
To enable strict mode, update your pyrightconfig.json
file:
{
"include": ["src"],
"exclude": ["venv"],
"pythonVersion": "3.13",
"typeCheckingMode": "strict"
}
Now, running Pyright on the following untyped function:
def greet(name):
return "Hello, " + name
Will produce this error:
src/main.py
src/main.py:1:5 - error: Return type is unknown (reportUnknownParameterType)
src/main.py:1:11 - error: Type of parameter "name" is unknown (reportUnknownParameterType)
src/main.py:1:11 - error: Type annotation is missing for parameter "name" (reportMissingParameterType)
src/main.py:2:12 - error: Return type is unknown (reportUnknownVariableType)
4 errors, 0 warnings, 0 informations
Watching for file changes...
To fix this error, add explicit type hints:
def greet(name: str) -> str:
return "Hello, " + name
0 errors, 0 warnings, 0 informations
Watching for file changes...
Strict mode helps enforce best practices, making your code more robust and maintainable.
Step 4 — Using advanced type hints with Pyright
Now that Pyright’s type checking is set up and working, the next step is to explore advanced type hints to improve type safety and make your code more maintainable.
Python’s typing
module allows for precise annotations, enabling Pyright to enforce stricter type checking while keeping the code flexible.
In this step, you will learn to use Optional
for handling None
values safely and Union
to allow multiple accepted types, ensuring flexibility while maintaining type safety.
Incorporating these hints, Pyright can help catch even more subtle bugs, ensuring your code remains robust.
Handling None
with Optional
By default, Pyright expects function parameters always to have a valid type. If a parameter or return value might be None
, it must be explicitly declared as Optional
.
Take a look at this:
from typing import Optional
def greet(name: Optional[str]) -> str:
if name is None:
return "Hello, Guest"
return f"Hello, {name}"
print(greet("Pyright")) # Works
print(greet(None)) # Works because Optional allows None
Since Optional[str]
allows both str
and None
, Pyright won't report any errors:
0 errors, 0 warnings, 0 informations
Watching for file changes...
However, if Optional
is removed and None
is passed, Pyright will raise an error:
def greet(name: str) -> str: # Optional is removed
...
src/main.py
src/main.py:1:20 - error: Import "Optional" is not accessed (reportUnusedImport)
src/main.py:5:8 - error: Condition will always evaluate to False since the types "str" and "None" have no overlap (reportUnnecessaryComparison)
src/main.py:11:13 - error: Argument of type "None" cannot be assigned to parameter "name" of type "str" in function "greet"
"None" is not assignable to "str" (reportArgumentType)
3 errors, 0 warnings, 0 informations
Watching for file changes...
Using Optional
ensures the function handles both None
and str
values correctly.
Allowing multiple types with Union
Sometimes, a function needs to accept multiple types. Instead of relying on runtime checks, you can explicitly specify the accepted types using Union
.
Here is an example:
from typing import Union
def greet(name: Union[str, int]) -> str:
return f"Hello, {name}"
print(greet("Pyright")) # Works
print(greet(42)) # Works because int is allowed
print(greet(3.14)) # This should trigger a type error
Running Pyright will detect the invalid argument:
src/main.py
src/main.py:10:13 - error: Argument of type "float" cannot be assigned to parameter "name" of type "str | int" in function "greet"
Type "float" is not assignable to type "str | int"
"float" is not assignable to "str"
"float" is not assignable to "int" (reportArgumentType)
1 error, 0 warnings, 0 informations
Watching for file changes...
Using Union[str, int]
ensures that only valid types are accepted.
Step 5 — Leveraging Pyright’s type narrowing
Now that you’ve learned how to use advanced type hints such as Optional
and Union
, it's time to explore type narrowing, one of Pyright’s most powerful features.
Type narrowing allows Pyright to dynamically infer and validate types within your code, reducing the need for explicit type casting and enhancing overall safety.
It refines types using conditional checks, applies isinstance()
for runtime validation, and detects guarded code paths to eliminate unnecessary checks.
Understanding type narrowing allows you to write cleaner, more reliable code while allowing Pyright to handle type inference intelligently.
Refining types with conditional checks
Pyright automatically narrows a variable’s type based on conditional logic. Consider the following example:
from typing import Union
def process_data(value: Union[str, int]) -> str:
if isinstance(value, int): # Pyright detects 'value' is now an int
return f"Processed number: {value * 2}"
return f"Processed string: {value.upper()}" # Pyright knows 'value' is a string here
print(process_data("hello")) # Works
print(process_data(10)) # Works
No errors are reported when Pyright runs:
0 errors, 0 warnings, 0 informations
Watching for file changes...
This is because Pyright detects that isinstance(value, int)
ensures value
must be an int
inside that block. After the if
statement, Pyright automatically infers that value
must be a str
.
Without type narrowing, you would need explicit type hints or cast()
, making the code more verbose.
Avoiding unnecessary comparisons
When Pyright knows that a variable can never be a certain type, it flags unnecessary checks.
Look at the following example:
def check_value(value: str) -> bool:
if value is None: # Pyright knows this is invalid
return False
return True
Running Pyright detects the issue:
src/main.py:2:8 - error: Condition will always evaluate to False since the types "str" and "None" have no overlap (reportUnnecessaryComparison)
1 error, 0 warnings, 0 informations
Watching for file changes...
If None
should be an allowed value, modify the function signature:
from typing import Optional
def check_value(value: Optional[str]) -> bool:
return value is not None
With this change, Pyright shows no errors:
0 errors, 0 warnings, 0 informations
Watching for file changes...
Type narrowing with assert
statements
Pyright can also infer types using assert
statements. This is useful for enforcing stricter conditions at runtime while keeping type safety.
Here is an example enforcing a non-None value:
from typing import Optional
def greet(name: Optional[str]) -> str:
assert name is not None, "Name cannot be None"
return f"Hello, {name}" # Pyright now treats 'name' as a guaranteed string
print(greet("Alice")) # Works
print(greet(None)) # Raises an AssertionError
0 errors, 0 warnings, 0 informations
Watching for file changes...
Since the assertion ensures name
is never None
, Pyright allows the string operation without complaints.
Using cast()
for explicit type conversions
In cases where Pyright cannot automatically narrow types, you can use cast()
from the typing
module to provide hints.
Take the following example:
from typing import cast, Any
def process_anything(data: Any) -> str:
name = cast(str, data) # Explicitly tell Pyright that 'data' is a string
return name.upper()
print(process_anything("hello")) # Works
print(process_anything(123)) # Still unsafe at runtime!
0 errors, 0 warnings, 0 informations
Watching for file changes...
While cast()
can suppress type errors, it does not perform any runtime checking, so use it cautiously.
Now that you’ve explored type narrowing, the next step explores type safety with Final
and Literal
.
Step 6 — Enforcing type safety with Final
and Literal
Now that you’ve explored type narrowing, it’s time to look at two powerful features in Pyright that help enforce stricter type safety: Final
and Literal
.
These features allow you to prevent unintended modifications and restrict values to predefined constants, reducing errors and improving code maintainability.
The Final
type ensures that a variable, once assigned, cannot be reassigned later in the program. This is particularly useful for defining constants or protecting attributes in classes.
For example, consider a mathematical constant like PI
. By marking it as Final
, you ensure that its value remains unchanged throughout the program.:
from typing import Final
PI: Final[float] = 3.14159 # This value cannot be changed
def calculate_area(radius: float) -> float:
return PI * radius * radius
print(calculate_area(5)) # Output: 78.53975
PI = 3.14 # This should trigger a type error
Running Pyright will catch the reassignment of PI
and raise an error, preventing accidental modifications.
src/main.py:12:1 - error: "PI" is constant (because it is uppercase) and cannot be redefined (reportConstantRedefinition)
src/main.py:12:1 - error: "PI" is declared as Final and cannot be reassigned (reportGeneralTypeIssues)
2 errors, 0 warnings, 0 informations
Watching for file changes...
The Final
type can also be applied to class attributes. In a configuration class, marking an attribute as Final
ensures that subclasses cannot override it:
from typing import Final
class Config:
APP_NAME: Final[str] = "MyApp" # Cannot be overridden in a subclass
class CustomConfig(Config):
APP_NAME = "NewApp" # This should trigger an error
src/main.py:9:5 - error: "APP_NAME" cannot be redeclared because parent class "Config" declares it as Final (reportGeneralTypeIssues)
1 error, 0 warnings, 0 informations
Watching for file changes...
Pyright will detect this modification attempt and prevent it. This is useful when designing configurations or constants that should remain unchanged across different parts of a project.
Another helpful feature in Pyright is Literal
, which restricts a variable or function argument to a fixed set of values.
This is especially helpful for enforcing predefined options, such as theme modes in an application:
from typing import Literal
def set_mode(mode: Literal["dark", "light"]) -> str:
return f"Mode set to {mode}"
print(set_mode("dark")) # Works
print(set_mode("light")) # Works
print(set_mode("blue")) # This should trigger a type error
When Pyright runs on this file, it will flag "blue"
as an invalid argument since it is not one of the predefined Literal
values.
src/main.py:10:16 - error: Argument of type "Literal['blue']" cannot be assigned to parameter "mode" of type "Literal['dark', 'light']" in function "set_mode"
Type "Literal['blue']" is not assignable to type "Literal['dark', 'light']"
"Literal['blue']" is not assignable to type "Literal['dark']"
"Literal['blue']" is not assignable to type "Literal['light']" (reportArgumentType)
1 error, 0 warnings, 0 informations
Watching for file changes...
In cases where a function should accept a fixed set of values but also allow None
, Literal
can be combined with Optional
. This ensures that the function receives a valid predefined option or no value:
from typing import Literal, Optional
def set_mode(mode: Optional[Literal["dark", "light"]]) -> str:
return f"Mode set to {mode}" if mode else "Mode not set"
print(set_mode("dark")) # Works
print(set_mode(None)) # Works
print(set_mode("blue")) # This should trigger a type error
Pyright will validate that only "dark"
, "light"
, or None
are allowed, catching any unintended values:
src/main.py:10:16 - error: Argument of type "Literal['blue']" cannot be assigned to parameter "mode" of type "Literal['dark', 'light'] | None" in function "set_mode"
Type "Literal['blue']" is not assignable to type "Literal['dark', 'light'] | None"
"Literal['blue']" is not assignable to "None"
"Literal['blue']" is not assignable to type "Literal['dark']"
"Literal['blue']" is not assignable to type "Literal['light']" (reportArgumentType)
1 error, 0 warnings, 0 informations
Watching for file changes...
With these tools, you can confidently use Pyright to enforce type safety and improve code reliability in your projects.
Final thoughts
This guide covered setting up Pyright, configuring type checking modes, using advanced type hints, and enforcing immutability with Final
and Literal
.
To explore more features, visit the official Pyright documentation and apply Pyright to your projects today.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github