AI Sparks

Pyright’s Type Coding Implementation for Testing Covering Generics, Protocols, Strict Mode, Type Reduction, and Modern Python Typing

In this lesson, we explore PyrightMicrosoft’s most efficient static type checker for Python, and packs its most powerful features into an efficient, Colab-friendly format. We start with basic annotations and type definitions, then move on to Union types, type reduction, generics, Protocols, TypedDict, data classes, and modern typing constructs like Self, TypeAlias, and NewType. We also explore how strict mode raises the bar for type safety across the codebase and how pyrightconfig.json provides fine-grained control over diagnostic rules at the project level. Also, we intentionally present both correct and intentionally broken code to see how Pyright catches real-world errors before they reach runtime.

Copy the CodeCopiedUse a different browser
import subprocess, sys, json, textwrap, os


subprocess.check_call([sys.executable, "-m", "pip", "install", "pyright", "-q"])
print("  pyright installedn")


WORK = "/tmp/pyright_tutorial"
os.makedirs(WORK, exist_ok=True)


def write(filename: str, code: str) -> str:
   path = os.path.join(WORK, filename)
   os.makedirs(os.path.dirname(path), exist_ok=True)
   with open(path, "w") as f:
       f.write(textwrap.dedent(code))
   return path


def run_pyright(*files, mode="basic", extra_flags=None):
   args = [sys.executable, "-m", "pyright",
           "--outputjson",
           f"--pythonversion=3.11",
           f"--typeCheckingMode={mode}",
           *(extra_flags or []),
           *[os.path.join(WORK, f) for f in files]]
   result = subprocess.run(args, capture_output=True, text=True)
   try:
       data = json.loads(result.stdout)
   except json.JSONDecodeError:
       print(result.stdout or result.stderr)
       return
   diags = data.get("generalDiagnostics", [])
   summary = data.get("summary", {})
   print(f"  errors={summary.get('errorCount',0)}  "
         f"warnings={summary.get('warningCount',0)}  "
         f"infos={summary.get('informationCount',0)}")
   for d in diags:
       sev = d["severity"].upper()
       msg = d["message"]
       rule = d.get("rule", "")
       line = d["range"]["start"]["line"] + 1
       fname = os.path.basename(d["file"])
       tag = f"[{rule}]" if rule else ""
       print(f"    {sev} {fname}:{line} — {msg} {tag}")
   if not diags:
       print("    (no diagnostics — all clean Pyright’s Type Coding Implementation for Testing Covering Generics, Protocols, Strict Mode, Type Reduction, and Modern Python Typing)") phrinta() phrinta("=" * 62) phrinta("ISIGABA 1 · Izichasiselo eziyisisekelo & inference") phrinta("=" * 62) bhala("s1_basics.py", """ def add(x: int, y: int) -> int: buyisela x + y umphumela: int = engeza(1, 2) engeza =(1) "engeza =(1) "hholo": "hello": 2, 3) def ukuphindaphinda(a: ntanta, b: ntanta): buyisela a * b x: str = phindaphinda(2.0, 3.0) """) phrinta("→ s1_basics.py (imodi eyisisekelo):") run_pyright("s1_basics.py") phrinta("=" *"Okokuzithandela 60 P2) phrinta i-syntax") phrinta("=" * 62) bhala("s2_optional_union.py", """ usuka ekubhaleni ukungenisa Okokuzithandela, I-Union def greet(igama: Ongakukhetha[str] = Lutho) -> str: uma igama Lingekho: buyisela okuthi "Sawubona, Isivakashi" buyisela f"Sawubona, {name}"bingelela("Alice") bingelela(Akekho) bingelela(42) def stringify(val: Union[int, float, bool]) -> str: buyisela i-str(val) stringify(3.14) stringify("x") def modern(val: int | str | Ayikho) -> str: buyisela "" uma i-val ithi Ayikho enye str(val) yesimanje(10) yesimanje([]) """) phrinta("→ s2_optional_union.py:") run_pyright("s2_optional_union.py")

We start by installing Pyright and setting up two helper functions, write and run_pyright, which we reuse throughout the following sections to create typed Python files and parse Pyright’s JSON diagnostic output. We then move on to the basics of type annotations, deliberately passing invalid argument types and mismatched assignments to see how Pyright flags each violation. We close this snippet by examining the Optional, Union, and modern pipe syntax of PEP 604, confirming that Pyright correctly rejects values ​​that fall outside the specified union of accepted types.

Copy the CodeCopiedUse a different browser
print("SECTION 3 · Type Narrowing")
print("=" * 62)


write("s3_narrowing.py", """
   from typing import Union
   import re


   def process(val: Union[int, str]) -> str:
       if isinstance(val, int):
           return str(val * 2)
       return val.upper()


   def safe_len(s: str | None) -> int:
       if s is None:
           return 0
       return len(s)


   def must_be_str(val: str | int) -> str:
       assert isinstance(val, str), "need string"
       return val.lower()


   from typing import TypeGuard


   def is_list_of_str(val: list[object]) -> TypeGuard[list[str]]:
       return all(isinstance(x, str) for x in val)


   def join_if_strings(lst: list[object]) -> str:
       if is_list_of_str(lst):
           return ", ".join(lst)
       return ""


   from typing import Literal, Never


   def assert_never(x: Never) -> Never:
       raise AssertionError(f"Unhandled: {x!r}")


   Status = Literal["ok", "error", "pending"]


   def handle(s: Status) -> str:
       match s:
           case "ok":      return "all good"
           case "error":   return "something failed"
           case "pending": return "waiting..."
""")


print("→ s3_narrowing.py:")
run_pyright("s3_narrowing.py")


print("=" * 62)
print("SECTION 4 · Generics, TypeVar, ParamSpec")
print("=" * 62)


write("s4_generics.py", """
   from typing import TypeVar, Generic, Callable, ParamSpec, Concatenate
   from collections.abc import Iterator


   T = TypeVar("T")
   S = TypeVar("S")
   P = ParamSpec("P")


   def first(lst: list[T]) -> T:
       return lst[0]


   x: int = first([1, 2, 3])
   y: str = first(["a", "b"])
   z: int = first(["a", "b"])


   class Stack(Generic[T]):
       def __init__(self) -> None:
           self._items: list[T] = []


       def push(self, item: T) -> None:
           self._items.append(item)


       def pop(self) -> T:
           return self._items.pop()


       def peek(self) -> T | None:
           return self._items[-1] if self._items else None


   int_stack: Stack[int] = Stack()
   int_stack.push(42)
   int_stack.push("oops")


   def logged(fn: Callable[P, T]) -> Callable[P, T]:
       def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
           print(f"calling {fn.__name__}")
           result = fn(*args, **kwargs)
           print(f"returned {result!r}")
           return result
       return wrapper


   @logged
   def add(a: int, b: int) -> int:
       return a + b


   add(1, 2)
   add("a", "b")


   Num = TypeVar("Num", int, float)


   def double(x: Num) -> Num:
       return x * 2  # type: ignore[operator]


   double(3)
   double(3.14)
   double("hi")
""")


print("→ s4_generics.py:")
run_pyright("s4_generics.py")

We dedicate this preview first to type reduction, one of Pyright’s most impressive capabilities, where we show how control flow builds like isstance, assert, TypeGuard, and structure game gradually develop type variables within each branch. Then we switch to generics, where we build a reusable stack[T] class and the first() function to show how TypeVar enables type-safe code that works across multiple concrete types without sacrificing index. We also introduce ParamSpec, which allows us to wrap functions with decorators while fully preserving the call’s original argument and return type signatures.

Copy the CodeCopiedUse a different browser
print("=" * 62)
print("SECTION 5 · Protocols & Structural Subtyping")
print("=" * 62)


write("s5_protocols.py", """
   from typing import Protocol, runtime_checkable


   @runtime_checkable
   class Drawable(Protocol):
       def draw(self) -> str: ...
       def area(self) -> float: ...


   class Circle:
       def __init__(self, r: float) -> None:
           self.r = r
       def draw(self) -> str:
           return f"○  r={self.r}"
       def area(self) -> float:
           return 3.14159 * self.r ** 2


   class Rectangle:
       def __init__(self, w: float, h: float) -> None:
           self.w, self.h = w, h
       def draw(self) -> str:
           return f"▭  {self.w}×{self.h}"
       def area(self) -> float:
           return self.w * self.h


   def render(shape: Drawable) -> None:
       print(shape.draw(), f"area={shape.area():.2f}")


   render(Circle(5.0))
   render(Rectangle(3.0, 4.0))
   render("not a shape")


   from typing import TypeVar, Generic


   T_co = TypeVar("T_co", covariant=True)


   class Readable(Protocol[T_co]):
       def read(self) -> T_co: ...


   class FileStream:
       def read(self) -> bytes:
           return b"data"


   def consume(source: Readable[bytes]) -> bytes:
       return source.read()


   consume(FileStream())
""")


print("→ s5_protocols.py:")
run_pyright("s5_protocols.py")


print("=" * 62)
print("SECTION 6 · TypedDict, dataclasses, NamedTuple")
print("=" * 62)


write("s6_datastructures.py", """
   from typing import TypedDict, NotRequired, Required
   from dataclasses import dataclass, field
   from typing import NamedTuple


   class User(TypedDict):
       id: int
       name: str
       email: NotRequired[str]


   u1: User = {"id": 1, "name": "Alice"}
   u2: User = {"id": 2, "name": "Bob", "email": "b@x"}
   u3: User = {"id": "bad", "name": "Eve"}


   def print_user(u: User) -> None:
       print(u["name"], u.get("email", "—"))


   @dataclass
   class Product:
       sku: str
       price: float
       tags: list[str] = field(default_factory=list)
       discount: float = 0.0


       def final_price(self) -> float:
           return self.price * (1 - self.discount)


   p = Product(sku="ABC", price=9.99)
   p.price = 12.50
   p.price = "free"


   class Point(NamedTuple):
       x: float
       y: float
       label: str = ""


   pt = Point(1.0, 2.0)
   dist: float = (pt.x ** 2 + pt.y ** 2) ** 0.5
   pt.x = 5.0
""")


print("→ s6_datastructures.py:")
run_pyright("s6_datastructures.py")

We explore Protocols in this snippet, which gives us a bit of structural typing: we can define an interface like Drawable and have Pyright accept any class that implements the necessary methods, unless that class inherits explicitly from Protocol. We extend this further with generic Readable[T] A protocol that combines structural checks with type parameters, showing how two elements are naturally named. We then turn to data structures, using TypedDict with NotRequired keys, @dataclass with typed fields, and NamedTuple to show how Pyright enforces type precision for all three of Python’s most common structured data patterns.

Copy the CodeCopiedUse a different browser
print("=" * 62)
print("SECTION 7 · Literal, Final, @overload")
print("=" * 62)


write("s7_literal_final_overload.py", """
   from typing import Literal, Final, overload


   Direction = Literal["north", "south", "east", "west"]


   def move(d: Direction, steps: int = 1) -> str:
       return f"Move {d} by {steps}"


   move("north")
   move("up")


   MAX_RETRIES: Final = 3
   MAX_RETRIES = 5


   class Config:
       DEBUG: Final[bool] = False
       VERSION: Final = "1.0.0"


   Config.DEBUG = True


   @overload
   def parse(value: str) -> int: ...
   @overload
   def parse(value: bytes) -> str: ...
   @overload
   def parse(value: int) -> float: ...


   def parse(value: str | bytes | int) -> int | str | float:
       if isinstance(value, str):
           return int(value)
       if isinstance(value, bytes):
           return value.decode()
       return float(value)


   a: int   = parse("42")
   b: str   = parse(b"hi")
   c: float = parse(99)
   d: int   = parse(99)
""")


print("→ s7_literal_final_overload.py:")
run_pyright("s7_literal_final_overload.py")


print("=" * 62)
print("SECTION 8 · Strict mode")
print("=" * 62)


write("s8_strict.py", """
   def no_annotation(x, y):
       return x + y


   class Bare:
       value = None


   def partial(x: int, y):
       return x
""")


print("→ s8_strict.py (strict mode):")
run_pyright("s8_strict.py", mode="strict")

We use literal types here to limit function conflicts to a fixed set of string values, and we pair that with Final to show how Pyright prevents reassignment of constants at both the module and class level. We introduce @overload to define multiple different call signatures for a single function, allowing Pyright to resolve the correct return type based on the precise type provided by the caller. We then switch to strict mode and run a deliberately under-annotated file with Pyright, revealing how many additional rules, missing return types, unwritten parameters, and implit Any strict mode are in effect compared to default.

Copy the CodeCopiedUse a different browser
print("=" * 62)
print("SECTION 9 · pyrightconfig.json")
print("=" * 62)


config = {
   "include": ["src"],
   "exclude": ["**/__pycache__"],
   "pythonVersion": "3.11",
   "typeCheckingMode": "strict",
   "reportMissingImports": "error",
   "reportMissingTypeStubs": "warning",
   "reportUnknownVariableType": "warning",
   "reportUnknownMemberType": "warning",
   "reportUnnecessaryTypeIgnoreComment": "warning",
}
cfg_path = os.path.join(WORK, "pyrightconfig.json")
with open(cfg_path, "w") as f:
   json.dump(config, f, indent=2)
print(f"Written: {cfg_path}")
print(json.dumps(config, indent=2))
print()


print("=" * 62)
print("SECTION 10 · Self, TypeAlias, NewType")
print("=" * 62)


write("s10_modern_types.py", """
   from typing import Self, TypeAlias, NewType


   class Query:
       def __init__(self) -> None:
           self._filters: list[str] = []


       def where(self, cond: str) -> Self:
           self._filters.append(cond)
           return self


       def build(self) -> str:
           return " AND ".join(self._filters)


   class AdvancedQuery(Query):
       def order_by(self, col: str) -> Self:
           return self


   q = AdvancedQuery().where("age > 18").order_by("name")
   reveal_type(q)


   Vector: TypeAlias = list[float]
   Matrix: TypeAlias = list[Vector]


   def dot(a: Vector, b: Vector) -> float:
       return sum(x * y for x, y in zip(a, b))


   v1: Vector = [1.0, 2.0, 3.0]
   v2: Vector = [4.0, 5.0, 6.0]
   dot(v1, v2)
   dot(v1, [1, 2, 3])


   UserId   = NewType("UserId", int)
   OrderId  = NewType("OrderId", int)


   def get_user(uid: UserId) -> str:
       return f"user_{uid}"


   uid = UserId(42)
   oid = OrderId(99)


   get_user(uid)
   get_user(oid)
   get_user(42)
""")


print("→ s10_modern_types.py:")
run_pyright("s10_modern_types.py")


print("=" * 62)
print("SECTION 11 · reveal_type() & type: ignore")
print("=" * 62)


write("s11_reveal_ignore.py", """
   from typing import Any


   values = [1, "two", 3.0]
   reveal_type(values)


   def mystery(x: Any) -> Any:
       return x


   r = mystery(42)
   reveal_type(r)


   bad: int = "oops"
   bad2: int = "also bad"  # type: ignore[assignment]
""")


print("→ s11_reveal_ignore.py:")
run_pyright("s11_reveal_ignore.py")


print("=" * 62)
print("TUTORIAL COMPLETE")
print("=" * 62)
print("""
Topics covered
──────────────
1  Basic annotations & inference
2  Optional / Union / PEP 604 syntax
3  Type narrowing (isinstance, guards, TypeGuard, match)
4  Generics — TypeVar, Generic, ParamSpec
5  Protocols & structural subtyping
6  TypedDict, dataclasses, NamedTuple
7  Literal, Final, @overload
8  Strict mode
9  pyrightconfig.json
10  Self, TypeAlias, NewType
11  reveal_type() & type: ignore


All source files written to: /tmp/pyright_tutorial/
""")

We write a pyrightconfig.json file in this snippet to show how to configure Pyright at the project level, enable strict mode globally and configure individual diagnostic rules, such as reportMissingImports and reportUnknownMemberType, to error or warning severity. We then work with three modern typing structures: Self for smart APIs that return the appropriate subclass type, TypeAlias ​​for readable type-level naming, and NewType for creating nominally unique types that Pyright refuses to merge even if their basic representation is the same.

In conclusion, we’ve covered eleven different areas of the Pyright type system and come up with a clear picture of how much safety and clear typing it brings to Python codebases of any size. We’ve seen that Pyright goes beyond simple annotation checks, limiting types with control flow, enforcing structural contracts with Protocols, maintaining callable signatures with ParamSpec, and strict closures with Final. We’ve also maintained a working workflow: we write typed code, run Pyright in basic or strict mode, interpret JSON diagnostic output, and tune behavior with pyrightconfig.json.


Check it out Full Codes with Notebook here. Also, feel free to follow us Twitter and don’t forget to join our 130k+ ML SubReddit and Subscribe to Our newspaper. Wait! are you on telegram? now you can join us on telegram too.

Need to work with us on developing your GitHub Repo OR Hug Face Page OR Product Release OR Webinar etc.?Connect with us

The post Pyright Type Coding Introduction to Standard Property Testing, Protocols, Strict Mode, Type Reduction, and Typing in Modern Python appeared first on MarkTechPost.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button