How Pyrefly Handles Enum Members and Their Literal Types
Pyrefly models Python Enum members as distinct singleton literal types using the Lit::Enum variant, converting them to TSP class types with the LITERAL flag to enable precise type narrowing and attribute resolution.
Pyrefly, Facebook’s open-source Python type checker, provides sophisticated handling for enum.Enum members that goes beyond basic type inference. By representing each enum member as a unique literal type with full access to its underlying class metadata and value type, Pyrefly enables accurate singleton type checking, read-only attribute semantics, and intelligent IDE quick-fixes.
Core Literal Representation
At the heart of Pyrefly’s enum handling is the LitEnum struct, which stores both the enum class reference and the member’s concrete value type.
In crates/pyrefly_types/src/literal.rs, the Lit enum defines how literal values are represented internally:
pub enum Lit {
// ... other variants ...
Enum(Box<LitEnum>), // ← enum member literal
// ...
}
pub struct LitEnum {
pub class: Class, // the enum class object
pub ty: Type, // the value type of the member (e.g. `int`, `str`)
}
This structure allows Pyrefly to preserve the relationship between the enum member (like Color.RED) and its underlying value type (such as int), which is crucial for resolving the .value attribute correctly.
Conversion to TSP Class Types
When Pyrefly converts internal literal representations to its Type-Signal-Protocol (TSP) system, enum members receive special treatment to mark them as singleton types.
In pyrefly/lib/tsp/type_conversion.rs (lines 509‑523), the conversion logic wraps enum literals with the LITERAL flag:
Lit::Enum(e) => {
// The enum class is used as the declaration source.
let cls = e.class.class_object();
let declaration = make_class_declaration(cls);
TspType::Class(TspClassType {
declaration: Declaration::Regular(declaration),
flags: TypeFlags::LITERAL, // ↳ marks a literal enum member
id: next_id(),
kind: TypeKind::Class,
literal_value: None,
// ...
})
}
The resulting TspClassType represents the specific enum member itself, not just the generic enum class. This distinction enables Pyrefly to treat Literal[Color.RED] as a distinct type that is incompatible with Literal[Color.BLUE], supporting exhaustive pattern matching and precise type narrowing.
Attribute Resolution for Enum Members
Enum members expose two critical attributes that Pyrefly resolves through specialized lookup logic in pyrefly/lib/alt/class/enums.rs.
The value and _value_ Attributes
When type-checking access to .value (or its internal alias ._value_), Pyrefly uses enum_value_lookup_on_member for instances and enum_value_lookup_on_class for class-level access:
- Instance access (
color.value): Returns the specific member’s value type stored inLitEnum.ty - Class access (
Color.RED.value): Computes the union of all member value types for the enum
This logic handles edge cases including mix-in classes, custom __new__ methods, and Django-specific enum patterns.
Read-Only Semantics
Pyrefly marks the value attribute with ReadOnlyReason::EnumMemberValue, making it read-only in all contexts except self.__init__, where a writable version is permitted during object construction.
Developer Experience Features
Beyond type checking, Pyrefly provides IDE integrations that catch common enum usage errors.
Quick-Fixes for Misspelled Members
When developers use string literals that match enum member names, Pyrefly suggests automatic replacements. The implementation in pyrefly/lib/state/lsp/quick_fixes/enum_member.rs defines the replace_with_enum_member_code_action function:
pub(crate) fn replace_with_enum_member_code_action(
module_info: &ModuleInfo,
ast: &ModModule,
error: &Error,
) -> Option<(String, Module, TextRange, String)> {
let replacement = enum_member_replacement(error)?;
let literal_range = enclosing_string_literal_range(ast, error.range())?;
Some((
format!("Replace with `{replacement}`"),
module_info.dupe(),
literal_range,
replacement.to_owned(),
))
}
This allows editors to offer one-click fixes converting "RED" to Color.RED when the type system detects a mismatch.
Enum Member Decorators
Functions decorated with @enum_member receive special treatment in pyrefly/lib/alt/function.rs (lines 769‑836). The decorator maps to SpecialDecorator::EnumMember, which constrains the function’s first argument to accept only specific enum members rather than the general enum class.
Practical Code Examples
The following patterns demonstrate how Pyrefly applies these internal mechanisms to real Python code:
from enum import Enum
from typing import Literal
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
# 1️⃣ Literal enum member – its type is a distinct singleton.
x: Literal[Color.RED] = Color.RED # ✅ accepted
# 2️⃣ Accessing the underlying value – Pyrefly infers `int`.
v: int = Color.RED.value # ✅ inferred as `int`
# 3️⃣ Using a misguided string literal – Pyrefly suggests a fix.
c: Literal["RED"] = "RED" # ❌ error
# quick-fix → Replace with `Color.RED`
# 4️⃣ Enum-member decorator constrains the first argument.
@enum_member
def handler(member: Color.RED) -> None:
reveal_type(member) # Revealed type is Literal[Color.RED]
Summary
- Literal Representation: Pyrefly stores enum members as
Lit::Enumcontaining the class object and value type incrates/pyrefly_types/src/literal.rs - Type Conversion: Enum literals convert to
TspClassTypewithTypeFlags::LITERALinpyrefly/lib/tsp/type_conversion.rs, creating singleton types - Attribute Lookup: The
valueattribute resolves viaenum_value_lookup_on_memberandenum_value_lookup_on_classinpyrefly/lib/alt/class/enums.rs, respecting read-only semantics - IDE Support: Misspelled enum strings trigger quick-fixes via
replace_with_enum_member_code_actioninpyrefly/lib/state/lsp/quick_fixes/enum_member.rs - Decorator Support: The
@enum_memberdecorator restricts function arguments to specific enum literals as implemented inpyrefly/lib/alt/function.rs
Frequently Asked Questions
How does Pyrefly represent Literal[MyEnum.A] internally?
Pyrefly represents Literal[MyEnum.A] as Lit::Enum(Box::new(LitEnum { class: <MyEnum>, ty: <value_type> })) in its internal type system. This is then converted to a TspClassType with the LITERAL flag set, creating a distinct singleton type that can be compared with other literals and used in unions according to the implementation in pyrefly/lib/tsp/type_conversion.rs.
Why does Pyrefly treat enum .value attributes as read-only?
Pyrefly marks enum .value attributes with ReadOnlyReason::EnumMemberValue to prevent accidental mutation of enum member values, which would violate the Enum contract in Python. The type checker only permits writing to .value within self.__init__ during object construction, as implemented in the attribute lookup logic in pyrefly/lib/alt/class/enums.rs.
Can Pyrefly suggest fixes when I use string literals instead of enum members?
Yes. When Pyrefly detects a string literal that matches an enum member name but is used in a context expecting the enum type, it generates a ReplaceWithEnumMember diagnostic with a quick-fix action. The LSP integration in pyrefly/lib/state/lsp/quick_fixes/enum_member.rs provides the exact replacement text (such as Color.RED) that editors can apply automatically.
What is the @enum_member decorator used for in Pyrefly?
The @enum_member decorator indicates that a function’s first parameter must be a specific enum member rather than the enum class itself. According to pyrefly/lib/alt/function.rs, this maps to SpecialDecorator::EnumMember and causes the type checker to validate that the argument matches a literal enum member type, enabling APIs that operate on specific enum variants rather than the general enum class.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →