Skip to content

typing TypeAlias #5945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6649,8 +6649,6 @@ def manager():
self.assertIsInstance(cm, typing.ContextManager)
self.assertNotIsInstance(42, typing.ContextManager)

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_contextmanager_type_params(self):
cm1 = typing.ContextManager[int]
self.assertEqual(get_args(cm1), (int, bool | None))
Expand Down
62 changes: 45 additions & 17 deletions compiler/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,33 +1067,41 @@ impl Compiler<'_> {

// For PEP 695 syntax, we need to compile type_params first
// so that they're available when compiling the value expression
// Push name first
self.emit_load_const(ConstantData::Str {
value: name_string.clone().into(),
});

if let Some(type_params) = type_params {
self.push_symbol_table();

// Compile type params first to define T1, T2, etc.
// Compile type params and push to stack
self.compile_type_params(type_params)?;
// Stack now has type_params tuple at top
// Stack now has [name, type_params_tuple]

// Compile value expression (can now see T1, T2)
self.compile_expression(value)?;
// Stack: [type_params_tuple, value]

// We need [value, type_params_tuple] for TypeAlias instruction
emit!(self, Instruction::Rotate2);
// Stack: [name, type_params_tuple, value]

self.pop_symbol_table();
} else {
// No type params - push value first, then None (not empty tuple)
self.compile_expression(value)?;
// Push None for type_params (matching CPython)
self.emit_load_const(ConstantData::None);
// Stack: [name, None]

// Compile value expression
self.compile_expression(value)?;
// Stack: [name, None, value]
}

// Push name last
self.emit_load_const(ConstantData::Str {
value: name_string.clone().into(),
});
emit!(self, Instruction::TypeAlias);
// Build tuple of 3 elements and call intrinsic
emit!(self, Instruction::BuildTuple { size: 3 });
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeAlias
}
);
self.store_name(&name_string)?;
}
Stmt::IpyEscapeCommand(_) => todo!(),
Expand Down Expand Up @@ -1246,12 +1254,22 @@ impl Compiler<'_> {
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(self, Instruction::TypeVarWithBound);
emit!(
self,
Instruction::CallIntrinsic2 {
func: bytecode::IntrinsicFunction2::TypeVarWithBound
}
);
} else {
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(self, Instruction::TypeVar);
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeVar
}
);
}

// Handle default value if present (PEP 695)
Expand All @@ -1274,7 +1292,12 @@ impl Compiler<'_> {
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(self, Instruction::ParamSpec);
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::ParamSpec
}
);

// Handle default value if present (PEP 695)
if let Some(default_expr) = default {
Expand All @@ -1296,7 +1319,12 @@ impl Compiler<'_> {
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(self, Instruction::TypeVarTuple);
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeVarTuple
}
);

// Handle default value if present (PEP 695)
if let Some(default_expr) = default {
Expand Down
46 changes: 7 additions & 39 deletions compiler/core/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,27 +382,13 @@ op_arg_enum!(
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum IntrinsicFunction1 {
/// Import * special case
// ImportStar = 0,
/// Set stop iteration value
// StopAsyncIteration = 1,
/// Unary operators
// UnaryPositive = 2,
// UnaryNegative = 3,
// UnaryNot = 4,
// UnaryInvert = 5,
/// Exit init subclass
// ExitInitCheck = 6,
/// Create a new list from an iterator
// ListToTupleForCall = 7,
/// Type parameter related
// TypeVar = 8,
// TypeVarTuple = 9,
// ParamSpec = 10,
TypeVar = 7,
ParamSpec = 8,
TypeVarTuple = 9,
/// Generic subscript for PEP 695
SubscriptGeneric = 10,
// TypeAlias = 12,
// TypeParams = 13,
TypeAlias = 11,
}
);

Expand All @@ -412,8 +398,8 @@ op_arg_enum!(
#[repr(u8)]
pub enum IntrinsicFunction2 {
// PrepReraiseS tar = 1,
// TypeVarWithBound = 2,
// TypeVarWithConstraints = 3,
TypeVarWithBound = 2,
TypeVarWithConstraint = 3,
SetFunctionTypeParams = 4,
/// Set default value for type parameter (PEP 695)
SetTypeparamDefault = 5,
Expand Down Expand Up @@ -668,16 +654,10 @@ pub enum Instruction {
MatchKeys,
MatchClass(Arg<u32>),
ExtendedArg,
TypeVar,
TypeVarWithBound,
TypeVarWithConstraint,
TypeAlias,
TypeVarTuple,
ParamSpec,
// If you add a new instruction here, be sure to keep LAST_INSTRUCTION updated
}
// This must be kept up to date to avoid marshaling errors
const LAST_INSTRUCTION: Instruction = Instruction::ParamSpec;
const LAST_INSTRUCTION: Instruction = Instruction::ExtendedArg;
const _: () = assert!(mem::size_of::<Instruction>() == 1);

impl From<Instruction> for u8 {
Expand Down Expand Up @@ -1380,12 +1360,6 @@ impl Instruction {
MatchKeys => -1,
MatchClass(_) => -2,
ExtendedArg => 0,
TypeVar => 0,
TypeVarWithBound => -1,
TypeVarWithConstraint => -1,
TypeAlias => -2,
ParamSpec => 0,
TypeVarTuple => 0,
}
}

Expand Down Expand Up @@ -1565,12 +1539,6 @@ impl Instruction {
MatchKeys => w!(MatchKeys),
MatchClass(arg) => w!(MatchClass, arg),
ExtendedArg => w!(ExtendedArg, Arg::<u32>::marker()),
TypeVar => w!(TypeVar),
TypeVarWithBound => w!(TypeVarWithBound),
TypeVarWithConstraint => w!(TypeVarWithConstraint),
TypeAlias => w!(TypeAlias),
ParamSpec => w!(ParamSpec),
TypeVarTuple => w!(TypeVarTuple),
}
}
}
Expand Down
126 changes: 61 additions & 65 deletions vm/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1255,71 +1255,6 @@ impl ExecutingFrame<'_> {
*extend_arg = true;
Ok(None)
}
bytecode::Instruction::TypeVar => {
let type_name = self.pop_value();
let type_var: PyObjectRef =
typing::TypeVar::new(vm, type_name.clone(), vm.ctx.none(), vm.ctx.none())
.into_ref(&vm.ctx)
.into();
self.push_value(type_var);
Ok(None)
}
bytecode::Instruction::TypeVarWithBound => {
let type_name = self.pop_value();
let bound = self.pop_value();
let type_var: PyObjectRef =
typing::TypeVar::new(vm, type_name.clone(), bound, vm.ctx.none())
.into_ref(&vm.ctx)
.into();
self.push_value(type_var);
Ok(None)
}
bytecode::Instruction::TypeVarWithConstraint => {
let type_name = self.pop_value();
let constraint = self.pop_value();
let type_var: PyObjectRef =
typing::TypeVar::new(vm, type_name.clone(), vm.ctx.none(), constraint)
.into_ref(&vm.ctx)
.into();
self.push_value(type_var);
Ok(None)
}
bytecode::Instruction::TypeAlias => {
let name = self.pop_value();
let type_params_obj = self.pop_value();

// CPython allows None or tuple for type_params
let type_params: PyTupleRef = if vm.is_none(&type_params_obj) {
// If None, use empty tuple (matching CPython's behavior)
vm.ctx.empty_tuple.clone()
} else {
type_params_obj
.downcast()
.map_err(|_| vm.new_type_error("Type params must be a tuple."))?
};

let value = self.pop_value();
let type_alias = typing::TypeAliasType::new(name, type_params, value);
self.push_value(type_alias.into_ref(&vm.ctx).into());
Ok(None)
}
bytecode::Instruction::ParamSpec => {
let param_spec_name = self.pop_value();
let param_spec: PyObjectRef = typing::ParamSpec::new(param_spec_name.clone(), vm)
.into_ref(&vm.ctx)
.into();
self.push_value(param_spec);
Ok(None)
}
bytecode::Instruction::TypeVarTuple => {
let type_var_tuple_name = self.pop_value();
let type_var_tuple: PyObjectRef =
typing::TypeVarTuple::new(type_var_tuple_name.clone(), vm)
.into_ref(&vm.ctx)
.into();
self.push_value(type_var_tuple);
Ok(None)
}
bytecode::Instruction::MatchMapping => {
// Pop the subject from stack
let subject = self.pop_value();
Expand Down Expand Up @@ -2272,6 +2207,53 @@ impl ExecutingFrame<'_> {
// Used for PEP 695: Generic[*type_params]
crate::builtins::genericalias::subscript_generic(arg, vm)
}
bytecode::IntrinsicFunction1::TypeVar => {
let type_var: PyObjectRef =
typing::TypeVar::new(vm, arg.clone(), vm.ctx.none(), vm.ctx.none())
.into_ref(&vm.ctx)
.into();
Ok(type_var)
}
bytecode::IntrinsicFunction1::ParamSpec => {
let param_spec: PyObjectRef = typing::ParamSpec::new(arg.clone(), vm)
.into_ref(&vm.ctx)
.into();
Ok(param_spec)
}
bytecode::IntrinsicFunction1::TypeVarTuple => {
let type_var_tuple: PyObjectRef = typing::TypeVarTuple::new(arg.clone(), vm)
.into_ref(&vm.ctx)
.into();
Ok(type_var_tuple)
}
bytecode::IntrinsicFunction1::TypeAlias => {
// TypeAlias receives a tuple of (name, type_params, value)
let tuple: PyTupleRef = arg
.downcast()
.map_err(|_| vm.new_type_error("TypeAlias expects a tuple argument"))?;

if tuple.len() != 3 {
return Err(vm.new_type_error(format!(
"TypeAlias expects exactly 3 arguments, got {}",
tuple.len()
)));
}

let name = tuple.as_slice()[0].clone();
let type_params_obj = tuple.as_slice()[1].clone();
let value = tuple.as_slice()[2].clone();

let type_params: PyTupleRef = if vm.is_none(&type_params_obj) {
vm.ctx.empty_tuple.clone()
} else {
type_params_obj
.downcast()
.map_err(|_| vm.new_type_error("Type params must be a tuple."))?
};

let type_alias = typing::TypeAliasType::new(name, type_params, value);
Ok(type_alias.into_ref(&vm.ctx).into())
}
}
}

Expand All @@ -2292,6 +2274,20 @@ impl ExecutingFrame<'_> {
arg1.set_attr("__type_params__", arg2, vm)?;
Ok(arg1)
}
bytecode::IntrinsicFunction2::TypeVarWithBound => {
let type_var: PyObjectRef =
typing::TypeVar::new(vm, arg1.clone(), arg2, vm.ctx.none())
.into_ref(&vm.ctx)
.into();
Ok(type_var)
}
bytecode::IntrinsicFunction2::TypeVarWithConstraint => {
let type_var: PyObjectRef =
typing::TypeVar::new(vm, arg1.clone(), vm.ctx.none(), arg2)
.into_ref(&vm.ctx)
.into();
Ok(type_var)
}
}
}

Expand Down
21 changes: 21 additions & 0 deletions vm/src/stdlib/typing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ pub(crate) mod decl {
value,
}
}

#[pygetset]
fn __name__(&self) -> PyObjectRef {
self.name.clone()
}

#[pygetset]
fn __value__(&self) -> PyObjectRef {
self.value.clone()
}

#[pygetset]
fn __type_params__(&self) -> PyTupleRef {
self.type_params.clone()
}

#[pymethod(name = "__repr__")]
fn repr(&self, vm: &VirtualMachine) -> PyResult<String> {
let name = self.name.str(vm)?;
Ok(name.as_str().to_owned())
}
Comment on lines +132 to +136
Copy link
Contributor

@coderabbitai coderabbitai bot Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the repr format matches Python's TypeAliasType behavior.

The current implementation returns only the alias name. Please verify this matches the expected behavior in Python 3.12+.


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that verify the __repr__ behavior
rg -A 5 "TypeAliasType.*__repr__" --type rust
rg -A 10 "test.*type.*alias.*repr" --type rust -i

Length of output: 99


🌐 Web query:

What is the expected __repr__ format for TypeAliasType in Python 3.12?

💡 Result:

In Python 3.12, the type statement and the introduction of the TypeAliasType object in typing provide structured, introspectable type aliases. When you define a type alias using the new syntax, for example:

type Vector = list[float]

Python internally creates an instance of TypeAliasType named Vector[4][2][5]. The expected __repr__ output for such a type alias reflects this new type object:

<TypeAliasType Vector>

This matches the pattern Python uses for many higher-level type objects, showing both the type (TypeAliasType) and the alias name (Vector). If you inspect the alias at runtime—e.g., with repr(Vector)—you should see output in this form[4].

For TypeAliasType instances constructed manually (rare in typical library code but possible for advanced users or tools), the format is the same:

from typing import TypeAliasType
MyAlias = TypeAliasType('MyAlias', list[int], ())
repr(MyAlias)
# Output: <TypeAliasType MyAlias>

This format—<TypeAliasType AliasName>—is standard and allows for clear introspection and debugging of type aliases in Python 3.12 and later[4].

Note: If you're using the TypeAlias annotation (not the new type statement), the object is just the type itself (e.g., <class 'list'> for Vector: TypeAlias = list[float]), not a TypeAliasType object, so its __repr__ will be the usual for that type[4][2]. The specialized __repr__ is specific to TypeAliasType created via the type statement or explicitly with TypeAliasType().

Summary table:

Alias Definition Style Object Type repr Output Example
type MyAlias = list[int] TypeAliasType <TypeAliasType MyAlias>
MyAlias: TypeAlias = list[int] underlying type <class 'list'>

This behavior is consistent with Python 3.12+ static typing changes and improves the introspection of aliases during development and debugging[4][5].

Citations:


Update __repr__ to match Python 3.12’s <TypeAliasType AliasName> format

The current implementation returns only the alias name (e.g. "Vector"), but Python 3.12’s TypeAliasType should repr as <TypeAliasType Vector>. Please update the method accordingly:

• File: vm/src/stdlib/typing.rs
Lines: 132–136

         #[pymethod(name = "__repr__")]
         fn repr(&self, vm: &VirtualMachine) -> PyResult<String> {
-            let name = self.name.str(vm)?;
-            Ok(name.as_str().to_owned())
+            let name = self.name.str(vm)?;
+            Ok(format!("<TypeAliasType {}>", name.as_str()))
         }

• Add a unit test (e.g. in tests/typing.rs) to assert that
repr(type Vector = list[float]) == "<TypeAliasType Vector>".
This will ensure your implementation aligns with upstream Python behavior.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[pymethod(name = "__repr__")]
fn repr(&self, vm: &VirtualMachine) -> PyResult<String> {
let name = self.name.str(vm)?;
Ok(name.as_str().to_owned())
}
#[pymethod(name = "__repr__")]
fn repr(&self, vm: &VirtualMachine) -> PyResult<String> {
let name = self.name.str(vm)?;
Ok(format!("<TypeAliasType {}>", name.as_str()))
}
🤖 Prompt for AI Agents
In vm/src/stdlib/typing.rs lines 132 to 136, update the repr method to return a
string formatted as "<TypeAliasType AliasName>" instead of just the alias name.
Modify the code to include the fixed prefix "<TypeAliasType " and suffix ">"
around the alias name. Additionally, add a unit test in tests/typing.rs that
creates a type alias like Vector = list[float] and asserts that its repr output
equals "<TypeAliasType Vector>" to verify compliance with Python 3.12 behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

>>> type A = object
>>> A
A
>>> repr(A)
'A'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@youknowone Thank you for the correction! You're absolutely right. The current implementation in the diff is correct - it returns just the alias name as a string, which matches the actual behavior in Python 3.12.

I apologize for the confusion in my previous analysis. The web search results were misleading about the __repr__ format. Your demonstration clearly shows that repr(A) returns 'A' (just the alias name), not <TypeAliasType A>.

The current implementation is working as expected and aligns with Python 3.12's actual behavior.


✏️ Learnings added
Learnt from: youknowone
PR: RustPython/RustPython#5945
File: vm/src/stdlib/typing.rs:132-136
Timestamp: 2025-07-11T07:16:06.209Z
Learning: In Python 3.12, TypeAliasType's __repr__ method returns just the alias name as a string (e.g., repr(A) returns 'A'), not the "<TypeAliasType AliasName>" format. This applies to type aliases created with the `type` statement syntax.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
PR: RustPython/RustPython#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-06-30T10:08:48.858Z
Learning: Applies to **/*.rs : Use the macro system (`pyclass`, `pymodule`, `pyfunction`, etc.) when implementing Python functionality in Rust

}

// impl AsMapping for Generic {
Expand Down