# Introduction to Pyright

[Pyright](https://github.com/microsoft/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](https://www.python.org/downloads/) 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:

```command
mkdir pyright-demo && cd pyright-demo
```

Next, create and activate a virtual environment:

```command
python3 -m venv venv
```
```command
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:

```command
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:

```command
pyright --version
```

If the installation is successful, this command will display the installed version of Pyright:

```text
[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:  

```json
[label 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:  

```python
[label 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:  

```command
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:

```text
[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:

```command
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:  

```python
[label 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:  

```text
[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:

```python
[label src/main.py]
def greet(name):
    return "Hello, " + name
```

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

```text
[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:  

```json
[label pyrightconfig.json]
{
  "include": ["src"],
  "exclude": ["venv"],
  "pythonVersion": "3.13",
[highlight]
  "typeCheckingMode": "strict"
[/highlight]
}
```

Now, running Pyright on the following untyped function:  

```python
def greet(name):
    return "Hello, " + name
```

Will produce this error:  

```text
[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:

```python
def greet(name: str) -> str:
    return "Hello, " + name
```
```text
[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:

```python
[label 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**:

```text
[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:

```python
[label src/main.py]
def greet(name: str) -> str:  #  Optional is removed
    ...
```

```text
[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:

```python
[label 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:

```text
[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:

```python
[label 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:

```text
[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:

```python
[label 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:

```text
[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:

```python
[label 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:

```text
[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:
  

```python
[label 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
```
```text
[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:

```python
[label 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!
```
```text
[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.:

```python
[label 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.  

```text
[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:

```python
[label 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
```

```text
[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:  

```python
[label 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.  

```text
[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:

```python
[label 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:

```text
[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](https://microsoft.github.io/pyright/#/) and apply Pyright to your projects today. 