|

A Coding Implementation on Pyright Type Checking Covering Generics, Protocols, Strict Mode, Type Narrowing, and Modern Python Typing

✅

In this tutorial, we discover Pyright, Microsoft’s high-performance static sort checker for Python, and stroll by means of its strongest options in a hands-on, Colab-friendly format. We begin from the bottom up with primary annotations and sort inference, then progressively advance by means of Union varieties, sort narrowing, generics, Protocols, TypedDict, dataclasses, and trendy typing constructs like Self, TypeAlias, and NewType. We additionally study how strict mode raises the bar for sort security throughout a whole codebase and how pyrightconfig.json offers fine-grained management over diagnostic guidelines on the mission stage. Also, we intentionally introduce each right and deliberately damaged code to see precisely how Pyright catches real-world errors earlier than they ever attain runtime.

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.be a part of(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(*information, mode="primary", 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]]
   outcome = subprocess.run(args, capture_output=True, textual content=True)
   attempt:
       information = json.hundreds(outcome.stdout)
   besides json.JSONDecodeError:
       print(outcome.stdout or outcome.stderr)
       return
   diags = information.get("generalDiagnostics", [])
   abstract = information.get("abstract", {})
   print(f"  errors={abstract.get('errorCount',0)}  "
         f"warnings={abstract.get('warningCount',0)}  "
         f"infos={abstract.get('informationCount',0)}")
   for d in diags:
       sev = d["severity"].higher()
       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 clear ✅)")
   print()


print("=" * 62)
print("SECTION 1 · Basic annotations & inference")
print("=" * 62)


write("s1_basics.py", """
   def add(x: int, y: int) -> int:
       return x + y


   outcome: int = add(1, 2)


   unhealthy: int = "good day"
   add(1, "two")
   add(1, 2, 3)


   def multiply(a: float, b: float):
       return a * b


   x: str = multiply(2.0, 3.0)
""")


print("→ s1_basics.py (primary mode):")
run_pyright("s1_basics.py")


print("=" * 62)
print("SECTION 2 · Optional / Union / PEP 604 syntax")
print("=" * 62)


write("s2_optional_union.py", """
   from typing import Optional, Union


   def greet(identify: Optional[str] = None) -> str:
       if identify is None:
           return "Hello, Guest"
       return f"Hello, {identify}"


   greet("Alice")
   greet(None)
   greet(42)


   def stringify(val: Union[int, float, bool]) -> str:
       return str(val)


   stringify(3.14)
   stringify("x")


   def trendy(val: int | str | None) -> str:
       return "" if val is None else str(val)


   trendy(10)
   trendy([])
""")


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

We start by putting in Pyright and organising two helper capabilities, write and run_pyright, which we reuse all through subsequent sections to create typed Python information and parse Pyright’s JSON diagnostic output cleanly. We then transfer on to the fundamentals of sort annotations, intentionally passing incorrect argument varieties and mismatched assignments to look at precisely how Pyright flags every violation. We shut this snippet by exploring Optional, Union, and the trendy PEP 604 pipe syntax, confirming that Pyright appropriately rejects values that fall outdoors the declared union of accepted varieties.

print("SECTION 3 · Type Narrowing")
print("=" * 62)


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


   def course of(val: Union[int, str]) -> str:
       if isinstance(val, int):
           return str(val * 2)
       return val.higher()


   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), "want string"
       return val.decrease()


   from typing import TypeGuard


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


   def join_if_strings(lst: listing[object]) -> str:
       if is_list_of_str(lst):
           return ", ".be a part of(lst)
       return ""


   from typing import Literal, Never


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


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


   def deal with(s: Status) -> str:
       match s:
           case "okay":      return "all good"
           case "error":   return "one thing failed"
           case "pending": return "ready..."
""")


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: listing[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: listing[T] = []


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


       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__}")
           outcome = fn(*args, **kwargs)
           print(f"returned {outcome!r}")
           return outcome
       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  # sort: ignore[operator]


   double(3)
   double(3.14)
   double("hello")
""")


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

We dedicate this snippet first to sort narrowing, one among Pyright’s most spectacular capabilities, the place we show how control-flow constructs equivalent to isinstance, assert, TypeGuard, and structural match progressively refine a variable’s sort inside every department. We then shift to generics, the place we construct a reusable Stack[T] class and a primary() operate to show how TypeVar permits type-safe code that works throughout a number of concrete varieties with out sacrificing inference. We additionally introduce ParamSpec, which permits us to wrap capabilities with decorators whereas absolutely preserving the unique callable’s argument and return sort signatures.

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 space(self) -> float: ...


   class Circle:
       def __init__(self, r: float) -> None:
           self.r = r
       def draw(self) -> str:
           return f"○  r={self.r}"
       def space(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 space(self) -> float:
           return self.w * self.h


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


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


   from typing import TypeVar, Generic


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


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


   class FileStream:
       def learn(self) -> bytes:
           return b"information"


   def eat(supply: Readable[bytes]) -> bytes:
       return supply.learn()


   eat(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, subject
   from typing import NamedTuple


   class User(TypedDict):
       id: int
       identify: str
       electronic mail: NotRequired[str]


   u1: User = {"id": 1, "identify": "Alice"}
   u2: User = {"id": 2, "identify": "Bob", "electronic mail": "b@x"}
   u3: User = {"id": "unhealthy", "identify": "Eve"}


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


   @dataclass
   class Product:
       sku: str
       value: float
       tags: listing[str] = subject(default_factory=listing)
       low cost: float = 0.0


       def final_price(self) -> float:
           return self.value * (1 - self.low cost)


   p = Product(sku="ABC", value=9.99)
   p.value = 12.50
   p.value = "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 discover Protocols on this snippet, which offer us with structural subtyping: we are able to outline an interface like Drawable and have Pyright settle for any class that implements the required strategies, with out that class ever explicitly inheriting from the Protocol. We lengthen this additional with a generic Readable[T] Protocol that mixes structural checking with sort parameters, displaying how the 2 options compose naturally. We then flip to information buildings, utilizing TypedDict with NotRequired keys, @dataclass with typed fields, and NamedTuple to show how Pyright enforces sort correctness throughout Python’s three commonest structured-data patterns.

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 transfer(d: Direction, steps: int = 1) -> str:
       return f"Move {d} by {steps}"


   transfer("north")
   transfer("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(worth: str) -> int: ...
   @overload
   def parse(worth: bytes) -> str: ...
   @overload
   def parse(worth: int) -> float: ...


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


   a: int   = parse("42")
   b: str   = parse(b"hello")
   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:
       worth = None


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


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

We use Literal varieties right here to limit a operate’s argument to a hard and fast set of string values, and we pair that with Final to indicate how Pyright prevents reassignment of constants at each the module and class stage. We introduce @overload to outline a number of distinct name signatures for a single operate, permitting Pyright to resolve the proper return sort primarily based on the precise argument sort the caller offers. We then swap to strict mode and run a intentionally under-annotated file by means of Pyright, which reveals simply what number of further guidelines, lacking return varieties, untyped parameters, and implicit Any strict mode prompts in comparison with the default.

print("=" * 62)
print("SECTION 9 · pyrightconfig.json")
print("=" * 62)


config = {
   "embody": ["src"],
   "exclude": ["**/__pycache__"],
   "pythonVersion": "3.11",
   "sortCheckingMode": "strict",
   "reportMissingImports": "error",
   "reportMissingTypeStubs": "warning",
   "reportUnknownVariableType": "warning",
   "reportUnknownMemberType": "warning",
   "reportUnnecessaryTypeIgnoreComment": "warning",
}
cfg_path = os.path.be a part of(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: listing[str] = []


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


       def construct(self) -> str:
           return " AND ".be a part of(self._filters)


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


   q = SuperiorQuery().the place("age > 18").order_by("identify")
   reveal_type(q)


   Vector: TypeAlias = listing[float]
   Matrix: TypeAlias = listing[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() & sort: ignore")
print("=" * 62)


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


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


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


   r = thriller(42)
   reveal_type(r)


   unhealthy: int = "oops"
   bad2: int = "additionally unhealthy"  # sort: ignore[assignment]
""")


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


print("=" * 62)
print("TUTORIAL COMPLETE")
print("=" * 62)
print("""
Topics coated
──────────────
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() & sort: ignore


All supply information written to: /tmp/pyright_tutorial/
""")

We write a pyrightconfig.json file on this snippet to indicate how we configure Pyright on the mission stage, enabling strict mode globally and tuning particular person diagnostic guidelines, equivalent to reportMissingImports and reportUnknownMemberType, to both error or warning severity. We then work by means of three trendy typing constructs: Self for fluent builder APIs that return the proper subclass sort, TypeAlias for readable type-level naming, and NewType for creating nominally distinct varieties that Pyright refuses to combine even when their underlying illustration is an identical. 

In conclusion, we’ve got coated eleven distinct areas of Pyright’s sort system and come away with a transparent image of simply how a lot security and readability static typing brings to Python codebases of any dimension. We have seen that Pyright goes effectively past easy annotation checking, it narrows varieties by means of management circulate, enforces structural contracts by means of Protocols, preserves callable signatures by means of ParamSpec, and locks down constants by means of Final. We additionally maintained a sensible workflow: we write typed code, run Pyright in primary or strict mode, interpret its JSON diagnostic output, and tune conduct by way of pyrightconfig.json.


Check out the Full Codes with Notebook here. Also, be happy to observe us on Twitter and don’t neglect to hitch our 130k+ ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.

Need to companion with us for selling your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar and so forth.? Connect with us

The put up A Coding Implementation on Pyright Type Checking Covering Generics, Protocols, Strict Mode, Type Narrowing, and Modern Python Typing appeared first on MarkTechPost.

Similar Posts