Back to Pyrefly

Pyrefly v0.60.0

release_notes/release-notes-v0.60.0.md

0.63.114.5 KB
Original Source

Pyrefly v0.60.0

Status: BETA Release date: April 07, 2026

Pyrefly v0.60.0 bundles 168 commits from 28 contributors.


✨ New & Improved

AreaWhat's new
Type Checking- You can now use recursive type aliases without running into expansion errors.
  • Type narrowing has been improved for tuple match subjects — individual variables in tuple expressions like match x, y: are now correctly narrowed by their corresponding sub-patterns.

  • Generator functions with union return annotations (e.g., Iterator[int] \| None) now correctly decompose each union member instead of failing with a type error.

  • type[X \| Y] is now correctly assignable to types.UnionType, matching runtime behavior where str \| None creates a types.UnionType object.

  • Unpack[Ts] and other invalid type expressions are now correctly rejected when used as TypeForm values, improving PEP 747 conformance.

  • Variables with declared annotations now preserve their annotated type when assigned a value of type Any, preventing Any from leaking through in assignments, for-loops, augmented assignments, and context managers.

  • Implicit literals at module scope are now promoted to their base types when read from functions or exported, so timeout = 100 gives int (not Literal[100]) when accessed cross-barrier, while MY_CONST = 100 preserves its literal type.

  • Enum member types are now preserved as literals instead of being promoted away, enabling more precise type checking for enum-based APIs.

  • auto() in enums now infers its type from _generate_next_value_ and mixed-in data types instead of always resolving to int.

  • TypeForm is now callable — TypeForm() validates its argument is a valid type expression and returns TypeForm[T].

  • Stringified type annotations are now parsed when the left-hand side of an annotated assignment is a TypeForm.

  • Stack overflow with mutually recursive type aliases has been fixed — cyclic aliases like type T = U; type U = T now return an error type instead of causing infinite recursion.

  • Cross-module class imports now correctly resolve __slots__ and class field metadata, fixing false positives where imported classes triggered spurious "not declared in __slots__" errors. | | Language Server | - Suppression comments now support backslash continuations — a # pyrefly: ignore above the first line of a backslash-continued expression applies to the whole expression.

  • Top-level # pyrefly: ignore-errors directives can now appear after module docstrings, relaxing placement requirements.

  • Unannotated function bodies are now analyzed in IDE mode even when check-unannotated-defs = false, enabling hover, goto-def, and other features inside those functions.

  • The summary line in pyrefly check output now shows how many warnings or info messages were hidden when --min-severity filters them out (e.g., "INFO 0 errors (12 warnings not shown)").

  • Hover over tuple elements now shows the type of the individual element instead of the whole tuple.

  • The provide-type endpoint now returns correct result types for operator expressions (e.g., +pos returns Literal[False] instead of the dunder method signature) and module-qualifies type alias names in function signatures.

  • Code lens "Run" and "Test" commands are now available for if __name__ == "__main__" blocks and pytest/unittest-style tests, enabling one-click execution from the IDE.

  • Workspace configuration can now be customized per-folder via the python.pyrefly.configPath VSCode setting, allowing each workspace to point to a different config file.

  • The LSP now shows pyright-equivalent warnings (InvalidAnnotation, MissingImport, UnknownName) in no-config mode, aligning with pyright's default behavior.

  • Hidden directory ancestors (e.g., ~/.codex/worktrees/XXXX/project/) no longer cause the entire project to be excluded — hidden-directory filtering now operates relative to the project root. | | Error Suppressions | - Multi-tool suppression parsing is now enabled — the report command recognizes suppression comments from seven tools: # type: ignore, # pyrefly: ignore, # pyright: ignore, # mypy: type: ignore, # pyre-fixme, # ty: ignore, and # zuban: ignore. | | Performance | - ClassType::clone is now O(1) instead of O(n) by using Arc instead of Box for TArgs, eliminating deep copies of type arguments on every clone. This provides a 14% wall-clock speedup on scikit-learn and eliminates a major CPU hotspot.

  • bind_boundmethod no longer clones the entire BoundMethodType — it now matches on &m.func directly, reducing CPU time by 13-21% on the colour/colour workload.

  • FunctionKind::Def now wraps FuncId in Arc instead of Box, making clone a single refcount bump instead of a heap allocation, reducing CPU time by an additional 21% on colour/colour.

  • A cross-call protocol conformance cache has been added to Solver, memoizing (got, protocol) pairs across is_subset_eq calls and providing a 40-48% CPU reduction on colour/colour.

  • Overload resolution now borrows the callable instead of cloning it for every candidate, deferring the clone to the single winning overload and reducing CPU time by ~1.5% on static-frame. | | Refactoring & Code Actions | - New "Move module" and "Move members" refactoring actions enable moving entire modules or lifting local functions/methods to top-level scope with automatic import/shim creation.

  • Duplicated helper functions (statement_removal_range, needs_pass_after_removal, reindent_statement) have been consolidated into extract_shared.rs. | | Build System Integration | - Linux ppc64le wheel builds are now available in CI, enabling official wheel distribution for the PPC community.

  • Module name consistency for extra file extensions (e.g., .cinc, .cconf) has been fixed — ModuleName::from_path() now keeps the extension as part of the module name when appropriate, ensuring handle construction and import resolution produce the same module name. | | Typestats / Report | - Slot-level coverage metrics replace function-centric annotation_completeness / type_completeness percentages, aligning with the typestats data model.

  • SlotCounts tracks n_typable, n_typed, n_any, n_untyped for each symbol, with coverage() and strict_coverage() methods.

  • FileReport has been replaced with ModuleReport / SymbolReport for production output, with SymbolReport as a tagged enum (kind: attr, function, class) and flattened SlotCounts.

  • Entity counting added to ReportSummary: n_functions, n_methods, n_function_params, n_method_params, n_classes, n_attrs, n_type_ignores.

  • Instance attributes from __init__/__new__/__post_init__ bodies are now extracted and reported.

  • Method aliases (e.g., __rand__ = __and__) are now detected and reported as duplicate SymbolReport::Function entries.

  • Implicit dunder return types (e.g., __init__, __bool__, __len__) are now excluded from coverage counting.

  • Protocol classes are now correctly handled — the class itself has n_typable=0, while method signatures still count toward coverage.

  • Private module symbols and attributes (single-underscore prefix like _method or _attr) are now filtered out, matching typestats behavior.

  • Overload implementation signatures are now excluded from the report (they are not part of the public API).

  • Cap'n Proto format support has been added for pysa reports via --report-pysa-format <json\|capnp>, enabling zero-copy reads on the pysa (OCaml) side. | | SCC Solver Refactoring | - The SCC solver has undergone extensive refactoring to simplify and clarify the state machine logic, including unifying NodeState and IterationNodeState, consolidating per-node state maps, and removing dead discovery code.

  • Phase 0 and Phase 1+ now execute through the same iterative bypass, eliminating behavioral splits between discovery and iteration.

  • SCC membership discovery timing is now explicitly documented — at push time we can determine a node IS in an SCC, but not that it is NOT (because K::solve can trigger a dependency chain that cycles back).

  • A likely bug in SCC merging has been fixed — when a new cycle back into an existing SCC is discovered during fixpoint iteration, the nodes are now correctly merged.

  • SCC read accessors, placeholder write methods, and completion detection have been consolidated and clarified.

  • The pending_completed_sccs field has been narrowed from Vec<Scc> to Option<Scc>, encoding the invariant that at most one SCC can complete per on_calculation_finished event.

  • Ancestor SCC ownership naming has been clarified to describe the real ownership invariant instead of implying an "ancestor iteration driver".

  • Top-SCC member re-entry handling has been deduplicated into a single helper. | | Dataclass / Enum / Protocol | - @dataclass is now correctly rejected when applied to Protocol, Enum, or TypedDict subclasses, matching mypy and pyright behavior.

  • namedtuple with a field named cls no longer collides with the synthesized __new__ method's first parameter — the internal parameter is now _cls.

  • Functional namedtuple, Enum, and TypedDict definitions now emit a warning (instead of an error) when the string name does not match the variable name.

  • Duplicate Protocol base classes (e.g., class P(Protocol, Protocol)) are now correctly detected and reported.

  • @override is now enforced on __init__ methods when the decorator is explicitly applied.

  • functools.lru_cache on properties now preserves property semantics, fixing "no attribute get" errors on _lru_cache_wrapper. | | Pydantic | - Type-of-expression data for pysa has been restructured to per-function grouping with deduplicated type tables, reducing shared memory writes from O(expressions) to O(functions) per module. |


🐛 bug fixes

We closed 27 bug issues this release 👏

  • #2976: Fixed an issue where yield from with a union return annotation (e.g., Iterator[tuple[Any, ...]] | Generator[dict[str, Any], None, Unknown]) incorrectly reported invalid-yield because the union members were not being decomposed before checking assignability.
  • #2968: Fixed a false positive bad-argument-type error when using metaclasses — type[Banana | Grape] is now correctly assignable to FruitMeta when Banana and Grape inherit from Fruit(metaclass=FrustMeta).
  • #2402: Fixed an issue where Pyrefly excluded all files when the project lived inside a hidden directory (e.g., ~/.codex/worktrees/XXXX/project/) because the **/.[!/.]*/** glob pattern matched hidden directory components anywhere in the absolute path, including ancestors above the project root.
  • #2863: Fixed an issue where bounded type variables in match subjects resolved to their bound instead of the actual narrowed type — tuple match subjects like match x, y: now correctly narrow individual variables by their corresponding sub-patterns.
  • #2980: Fixed an issue where namedtuple with a field named cls caused type warnings because the field collided with the synthesized __new__ method's first parameter — the internal parameter is now _cls.
  • #2922: Fixed an issue where applying @dataclass to an Enum subclass was not rejected — this is now correctly reported as BadClassDefinition because Python's dataclasses module does not support Enum subclasses (runtime TypeError).
  • #2923: Fixed an issue where applying @dataclass to a TypedDict subclass was not rejected — this is now correctly reported in the canonical class_metadata_of path, and dataclass_metadata is cleared after reporting the error.
  • #2921: Fixed an issue where applying @dataclass to a Protocol subclass was not rejected — this is now correctly reported because Protocol classes define structural interfaces, not data containers.
  • #2975: Fixed an issue where the provide-type endpoint returned Unknown for type aliases — operator expressions now return result types instead of dunder method signatures, and type alias names in function signatures are now module-qualified.
  • #2982: Fixed a stack overflow crash when checking fuzzed code with mutually recursive type aliases like type T = U; type U = T — when a cycle is detected, wrap_type_alias now returns an error type instead of the cyclic body.
  • And more! #2987, #2610, #2874, #2857, #1950, #2741, #2983, #2919, #2979, #2973, #1982, #2910, #2950, #2978, #2875, #2986, #2972, #2852

Thank-you to all our contributors who found these bugs and reported them! Did you know this is one of the most helpful contributions you can make to an open-source project? If you find any bugs in Pyrefly we want to know about them! Please open a bug report issue here


📦 Upgrade

bash
pip install --upgrade pyrefly==0.60.0

How to safely upgrade your codebase

Upgrading the version of Pyrefly you're using or a third-party library you depend on can reveal new type errors in your code. Fixing them all at once is often unrealistic. We've written scripts to help you temporarily silence them. After upgrading, follow these steps:

  1. pyrefly check --suppress-errors
  2. run your code formatter of choice
  3. pyrefly check --remove-unused-ignores
  4. Repeat until you achieve a clean formatting run and a clean type check.

This will add # pyrefly: ignore comments to your code, enabling you to silence errors and return to fix them later. This can make the process of upgrading a large codebase much more manageable.

Read more about error suppressions in the Pyrefly documentation


🖊️ Contributors this release

@stroxler, @yangdanny97, @migeed-z, @rchen152, @javabster, @asukaminato0721, @kinto0, @avikchaudhuri, @arthaud, @tejasreddyvepala, @lolpack, @fangyi-zhou, @Louisvranderick, @runlevel5, @yeetypete, @NathanTempest, @stanleyshen2003, Morgan Bartholomew, @connernilsen, @salvatorebenedetto, @kshitijgetsac, @Raf-Hs, Mick Killianey, David Tolnay, @dhleong, @Arths17


Please note: These release notes summarize major updates and features. For brevity, not all individual commits are listed. Highlights from patch release changes that were shipped after the previous minor release are incorporated here as well.