From 8eaefbb7c1602a962d659ee7f9b3696c091d887f Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 25 Feb 2026 08:40:25 +0100 Subject: [PATCH 1/3] [Python] Fix type var scoping for PEP 695 annotations Prevent generic type params from leaking outside their function scope: - Emit `Any` for type vars outside function type param scope (e.g., class fields, variable annotations) instead of referencing undefined type vars - Only add actually-declared (repeated) generic params to ScopedTypeParams, so non-repeated params erased to Any don't leak - Remove test_mailbox_processor.py from pyright CI exclude list (now passes) Co-Authored-By: Claude Opus 4.6 --- pyrightconfig.ci.json | 1 - src/Fable.Cli/CHANGELOG.md | 4 ++++ src/Fable.Compiler/CHANGELOG.md | 4 ++++ src/Fable.Transforms/Python/Fable2Python.Annotation.fs | 6 ++++-- src/Fable.Transforms/Python/Fable2Python.Transforms.fs | 6 ++++-- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pyrightconfig.ci.json b/pyrightconfig.ci.json index ca44e111b..08b7c4fa0 100644 --- a/pyrightconfig.ci.json +++ b/pyrightconfig.ci.json @@ -5,7 +5,6 @@ "**/node_modules/**", "temp/tests/Python/test_applicative.py", "temp/tests/Python/test_hash_set.py", - "temp/tests/Python/test_mailbox_processor.py", "temp/tests/Python/test_nested_and_recursive_pattern.py", "temp/tests/Python/fable_modules/thoth_json_python/encode.py" ] diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index d2da0ede4..56b34bb2a 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [Beam] Add Erlang/BEAM target (`--lang beam`). Compiles F# to `.erl` source files. 2086 tests passing. (by @dbrattli) +### Fixed + +* [Python] Fix type var scoping for PEP 695 annotations: emit `Any` for type vars outside function scope and prevent non-repeated generic params from leaking into `ScopedTypeParams` (by @dbrattli) + ## 5.0.0-alpha.24 - 2026-02-13 ### Fixed diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 639c04bd5..475e29699 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [Beam] Add Erlang/BEAM compilation support: Fable2Beam transform, Beam Replacements, ErlangPrinter, and 31 runtime `.erl` modules. 2086 tests passing. (by @dbrattli) +### Fixed + +* [Python] Fix type var scoping for PEP 695 annotations: emit `Any` for type vars outside function scope and prevent non-repeated generic params from leaking into `ScopedTypeParams` (by @dbrattli) + ## 5.0.0-alpha.23 - 2026-02-13 ### Fixed diff --git a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs index d1baf0d60..0df69428e 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs @@ -424,8 +424,10 @@ let rec typeAnnotation com.AddTypeVar(ctx, name), [] | Some _ -> stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics | None -> - let name = Helpers.clean name - com.AddTypeVar(ctx, name), [] + // No repeatedGenerics info means we're outside a function type param scope + // (e.g., class fields, variable annotations). With PEP 695, type vars are + // lexically scoped, so emit Any instead of an undefined type var reference. + stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics | Fable.Unit -> Expression.none, [] | Fable.Boolean -> Expression.name "bool", [] | Fable.Char -> Expression.name "str", [] diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 46e242afa..3bcda3d9b 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3022,8 +3022,10 @@ let transformFunction let mutable isTailCallOptimized = false let argTypes = args |> List.map (fun id -> id.Type) - let genTypeParams = getGenericTypeParams (argTypes @ [ body.Type ]) - let newTypeParams = Set.difference genTypeParams ctx.ScopedTypeParams + // Only add actually-declared (repeated) generic params to scope. + // With PEP 695, type params are lexically scoped to their function declaration, + // so non-repeated params (erased to Any) must not leak into ScopedTypeParams. + let newTypeParams = Set.difference repeatedGenerics ctx.ScopedTypeParams let ctx = { ctx with From 09bb680d5dcb6e71ee93284c1816e77e6e56ca17 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 25 Feb 2026 18:40:40 +0100 Subject: [PATCH 2/3] [Python] Revert ScopedTypeParams change to fix nested type var scoping Revert to using getGenericTypeParams for ScopedTypeParams so that all outer function type params are tracked, preventing inner functions from re-declaring them in PEP 695 syntax ("TypeVar X is already in use by an outer scope"). The annotation change (emit Any outside function scope) is kept as that was the actual fix for the mailbox_processor issue. Co-Authored-By: Claude Opus 4.6 --- src/Fable.Transforms/Python/Fable2Python.Transforms.fs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 3bcda3d9b..46e242afa 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3022,10 +3022,8 @@ let transformFunction let mutable isTailCallOptimized = false let argTypes = args |> List.map (fun id -> id.Type) - // Only add actually-declared (repeated) generic params to scope. - // With PEP 695, type params are lexically scoped to their function declaration, - // so non-repeated params (erased to Any) must not leak into ScopedTypeParams. - let newTypeParams = Set.difference repeatedGenerics ctx.ScopedTypeParams + let genTypeParams = getGenericTypeParams (argTypes @ [ body.Type ]) + let newTypeParams = Set.difference genTypeParams ctx.ScopedTypeParams let ctx = { ctx with From b32def579854997be7f3a2551126513b593e5d9f Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 25 Feb 2026 19:03:29 +0100 Subject: [PATCH 3/3] [Python] Fix type var scoping: track only declared PEP 695 type params Two-part fix for PEP 695 type var scoping: 1. In transformFunction, only add repeated (actually-declared) generic params to ScopedTypeParams. Non-repeated params erased to Any must not enter scope, or inner functions will emit undefined type var references (e.g., Async[_A] where _A is never declared). 2. In transformModuleFunction, compute the declared type params (explicit generics + signature generics) BEFORE transforming the body and add them to ScopedTypeParams. This prevents inner functions from re-declaring type vars that the outer function already declares in its PEP 695 [] syntax. Co-Authored-By: Claude Opus 4.6 --- .../Python/Fable2Python.Transforms.fs | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 46e242afa..097022d85 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3022,8 +3022,11 @@ let transformFunction let mutable isTailCallOptimized = false let argTypes = args |> List.map (fun id -> id.Type) - let genTypeParams = getGenericTypeParams (argTypes @ [ body.Type ]) - let newTypeParams = Set.difference genTypeParams ctx.ScopedTypeParams + // Only track actually-declared (repeated) generic params in ScopedTypeParams. + // With PEP 695, type params are lexically scoped, so only params that appear + // in the function's [] declaration should be tracked. Non-repeated params + // (erased to Any) must not enter scope or inner functions will reference them. + let newTypeParams = Set.difference repeatedGenerics ctx.ScopedTypeParams let ctx = { ctx with @@ -3739,13 +3742,35 @@ let transformModuleFunction (info: Fable.MemberFunctionOrValue) (membName: string) (fableArgs: Fable.Ident list) - body + (body: Fable.Expr) = + let argTypes = fableArgs |> List.map _.Type + + // Compute which type params will be declared in this function's PEP 695 [] + // BEFORE transforming the body, so inner functions know these are already in scope + // and won't re-declare them (which would cause "TypeVar already in use" errors). + let explicitGenerics = Annotation.getMemberGenParams info.GenericParameters + + let signatureGenerics = + (getGenericTypeParams (argTypes @ [ body.Type ]), ctx.ScopedTypeParams) + ||> Set.difference + + let declaredTypeParams = + Set.empty + |> Set.union explicitGenerics + |> Set.union signatureGenerics + |> Set.difference + <| ctx.ScopedTypeParams + + let ctxWithDeclaredParams = + { ctx with ScopedTypeParams = Set.union ctx.ScopedTypeParams declaredTypeParams } + let args, body', returnType = - getMemberArgsAndBody com ctx (NonAttached membName) info.HasSpread fableArgs body + getMemberArgsAndBody com ctxWithDeclaredParams (NonAttached membName) info.HasSpread fableArgs body + + let typeParams = + Annotation.makeFunctionTypeParamsWithConstraints com ctx info.GenericParameters declaredTypeParams - let argTypes = fableArgs |> List.map _.Type - let typeParams = calculateTypeParams com ctx info argTypes body.Type let name = com.GetIdentifier(ctx, membName |> Naming.toPythonNaming) let isAsync = isTaskType body.Type