Back to Scaling Python Applications guides

Introduction to Pyright

Stanley Ulili
Updated on February 21, 2025

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:

Output
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:

pyrightconfig.json
{
  "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:

src/main.py
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:

Output
...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:

src/label.py
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:

Output
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:

src/main.py
def greet(name):
    return "Hello, " + name

Even though name lacks a type hint, Pyright assumes it could be any type.

Output
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:

pyrightconfig.json
{
  "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:

Output
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
Output
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:

src/main.py
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:

Output
0 errors, 0 warnings, 0 informations 
Watching for file changes...

However, if Optional is removed and None is passed, Pyright will raise an error:

src/main.py
def greet(name: str) -> str:  #  Optional is removed
    ...
Output
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:

src/main.py
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:

Output
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:

src/main.py
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:

Output
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:

src/main.py
def check_value(value: str) -> bool:
    if value is None:  # Pyright knows this is invalid
        return False
    return True

Running Pyright detects the issue:

Output
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:

src/main.py
from typing import Optional  

def check_value(value: Optional[str]) -> bool:
    return value is not None

With this change, Pyright shows no errors:

Output
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:

src/main.py
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
Output
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:

src/main.py
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!
Output
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.:

src/main.py
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.

Output
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:

src/main.py
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
Output
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:

src/main.py
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.

Output
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:

src/main.py
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:

Output
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.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Introduction to Mypy
Learn how to use Mypy, Python’s powerful static type checker, to improve code quality, catch type errors early, and enforce type safety. This comprehensive guide covers installation, configuration, advanced type hints, type narrowing, generics, and type coverage reports. Start writing cleaner, more reliable Python code today
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

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
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
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.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github