Agents convert-python-fsharp
Convert Python code to idiomatic F#. Use when migrating Python projects to F#, translating Python patterns to idiomatic F#, or refactoring Python codebases for type safety, functional programming, and .NET integration. Extends meta-convert-dev with Python-to-F# specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-python-fsharp" ~/.claude/skills/arustydev-agents-convert-python-fsharp && rm -rf "$T"
content/skills/convert-python-fsharp/SKILL.mdConvert Python to F#
Convert Python code to idiomatic F#. This skill extends
meta-convert-dev with Python-to-F# specific type mappings, idiom translations, and tooling for transforming dynamic, garbage-collected Python code into functional-first, statically-typed F# on the .NET platform.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Python types → F# types (dynamic → static)
- Idiom translations: Imperative/OOP Python → functional-first F#
- Error handling: Exceptions → Result/Option types
- Async patterns: asyncio → Async workflows
- Type system: Duck typing → discriminated unions + type inference
- Collection patterns: List comprehensions → List/Seq expressions + pipe operator
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Python language fundamentals - see
lang-python-dev - F# language fundamentals - see
lang-fsharp-dev - Reverse conversion (F# → Python) - see
or Fable.Python transpilerconvert-fsharp-python
Quick Reference
| Python | F# | Notes |
|---|---|---|
| , , | F# int is 32-bit, Python has arbitrary precision |
| | IEEE 754 double precision |
| | Direct mapping |
| | Immutable by default in both |
| | Byte array |
| | F# lists are immutable, linked |
(mutable) | | .NET List<T> |
| | Tuple syntax |
| | Immutable map |
(mutable) | | .NET Dictionary |
| | Immutable set |
| (in ) | Explicit nullable |
| Discriminated union | Tagged union |
| | Function type |
| | Async computation expression |
| | F# records |
| or | Railway-oriented programming preferred |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Embrace immutability - F# defaults to immutable; use mutable sparingly
- Adopt functional patterns - don't write "Python code in F# syntax"
- Use type inference - F# infers most types; annotate only when needed
- Railway-oriented programming - prefer Result/Option over exceptions
- Leverage pipe operator - chain operations with
|> - Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Python | F# | Notes |
|---|---|---|
| | 32-bit signed integer |
(large) | | 64-bit signed integer |
(arbitrary) | | Arbitrary precision (like Python) |
| | 64-bit floating point (F# = .NET ) |
| | Direct mapping |
| | UTF-16 immutable string (.NET) |
| | Byte array |
| | Mutable byte array |
| | Must be wrapped in |
(Ellipsis) | - | No direct equivalent |
Critical Note on Integers: Python's
int type has arbitrary precision and never overflows. F# int is 32-bit (like C#). Use bigint for Python-like arbitrary precision, or int64 for most cases.
Collection Types
| Python | F# | Notes |
|---|---|---|
| | F# list is immutable, singly-linked |
(mutable) | | .NET (mutable, growable) |
| | Fixed-size, immutable |
| | Immutable map (tree-based) |
(mutable) | | .NET (hash-based) |
| | Immutable set |
(mutable) | | .NET |
| | Immutable by default in F# |
| | .NET |
| Use | Preserve insertion order |
| + | Use pattern |
| | Count occurrences |
Composite Types
| Python | F# | Notes |
|---|---|---|
(data) | | F# records are immutable by default |
(behavior) | + member methods | OOP supported but not idiomatic |
| | Records with structural equality |
| Interface | Structural typing → nominal in F# |
| | Named fields |
| | Prefer records over tuples |
| Discriminated union | `type Color = Red |
| Discriminated union | `type Status = Active |
| `type Result = A of 'T | B of 'U` |
| | Explicit nullable |
| | Function type |
| | Generic type parameter |
Type Annotations → Generics
| Python | F# | Notes |
|---|---|---|
| | Unconstrained generic (usually inferred) |
| | F# is lazy |
| | Or for arrays |
| Avoid - use generics | exists but discouraged |
| | Root type, but use generics instead |
Idiom Translation
Pattern 1: None Handling (Optional Chaining)
Python:
# Optional chaining with walrus operator if user := get_user(user_id): name = user.name else: name = "Anonymous" # Or simpler name = user.name if user else "Anonymous"
F#:
// Option pattern matching let name = match get_user user_id with | Some user -> user.Name | None -> "Anonymous" // Or with defaultArg let name = get_user user_id |> Option.map (fun u -> u.Name) |> Option.defaultValue "Anonymous"
Why this translation:
- Python uses truthiness while F# uses explicit
Option<'T> - F# pattern matching is exhaustive (compiler ensures all cases handled)
- Pipe operator
chains operations left-to-right (like UNIX pipes)|>
Pattern 2: List Comprehensions → List/Seq Expressions
Python:
# List comprehension squared_evens = [x * x for x in numbers if x % 2 == 0] # Generator expression total = sum(x * x for x in numbers if x % 2 == 0)
F#:
// List expression let squaredEvens = [ for x in numbers do if x % 2 = 0 then x * x ] // Or with pipe operator (more idiomatic) let squaredEvens = numbers |> List.filter (fun x -> x % 2 = 0) |> List.map (fun x -> x * x) // Seq for lazy evaluation (like generator) let total = numbers |> Seq.filter (fun x -> x % 2 = 0) |> Seq.map (fun x -> x * x) |> Seq.sum
Why this translation:
- F# has both list expressions (like comprehensions) and pipe chains
- Pipe operator style is more composable and idiomatic
is lazy (like Python generators),Seq<'T>
is eagerList<'T>
Pattern 3: Dictionary/Map Operations
Python:
# Create dictionary counts = {"apple": 5, "banana": 3} # Add/update counts["orange"] = 2 counts["apple"] += 1 # Safe get with default count = counts.get("grape", 0)
F#:
// Create immutable map let counts = Map.ofList [ "apple", 5 "banana", 3 ] // Add/update (returns new map) let counts2 = counts |> Map.add "orange" 2 let counts3 = counts2 |> Map.add "apple" 6 // Safe get with default let count = counts |> Map.tryFind "grape" |> Option.defaultValue 0 // Or for mutable dictionary (.NET) let mutableCounts = System.Collections.Generic.Dictionary<string, int>() mutableCounts.["apple"] <- 5 mutableCounts.["apple"] <- mutableCounts.["apple"] + 1
Why this translation:
- F# defaults to immutable
(functional style)Map<'K, 'V> - Operations return new maps rather than mutating in-place
- Can use .NET
for mutable operations when neededDictionary
Pattern 4: Class → Record + Functions
Python:
from dataclasses import dataclass @dataclass class Point: x: float y: float def distance_from_origin(self) -> float: return (self.x ** 2 + self.y ** 2) ** 0.5
F#:
// F# record type type Point = { X: float Y: float } // Standalone function (idiomatic F#) let distanceFromOrigin point = sqrt (point.X ** 2.0 + point.Y ** 2.0) // Or as member method if needed type Point with member this.DistanceFromOrigin() = sqrt (this.X ** 2.0 + this.Y ** 2.0) // Usage let p = { X = 3.0; Y = 4.0 } let dist = distanceFromOrigin p // Functional style let dist2 = p.DistanceFromOrigin() // OOP style
Why this translation:
- F# records provide structural equality automatically
- Separating data (record) from functions is more functional
- Member methods available but less idiomatic than standalone functions
Pattern 5: Iteration and Loops → Recursion/Higher-Order Functions
Python:
# Imperative loop def factorial(n: int) -> int: result = 1 for i in range(1, n + 1): result *= i return result
F#:
// Recursive function (idiomatic F#) let rec factorial n = match n with | 0 | 1 -> 1 | _ -> n * factorial (n - 1) // Or tail-recursive (better for large n) let factorial n = let rec loop acc n = match n with | 0 | 1 -> acc | _ -> loop (acc * n) (n - 1) loop 1 n // Or using fold (most functional) let factorial n = [1..n] |> List.fold (*) 1
Why this translation:
- F# favors recursion and higher-order functions over loops
- Tail recursion is optimized by F# compiler
,fold
,map
express intent more clearly than loopsfilter
Pattern 6: Context Managers → use Binding
Python:
# Context manager with open("file.txt", "r") as f: content = f.read() # f is automatically closed
F#:
// use binding (implements IDisposable) use file = System.IO.File.OpenText("file.txt") let content = file.ReadToEnd() // file is automatically disposed at end of scope // Or with explicit scope let content = use file = System.IO.File.OpenText("file.txt") file.ReadToEnd()
Why this translation:
- F#
binding callsuse
automaticallyDispose() - Both Python and F# ensure resource cleanup
- F# leverages .NET's
patternIDisposable
Error Handling
Python Exceptions → F# Result Type
Python's exception model:
def divide(a: float, b: float) -> float: if b == 0: raise ValueError("Cannot divide by zero") return a / b try: result = divide(10, 0) except ValueError as e: print(f"Error: {e}") result = 0
F# Result type (idiomatic):
// Define error type type MathError = | DivideByZero | InvalidInput of string // Function returns Result<'T, 'E> let divide a b = if b = 0.0 then Error DivideByZero else Ok (a / b) // Pattern match on result let result = match divide 10.0 0.0 with | Ok value -> value | Error DivideByZero -> printfn "Error: Cannot divide by zero" 0.0 | Error (InvalidInput msg) -> printfn "Error: %s" msg 0.0
F# with try/with (when needed):
// F# also supports exceptions for interop let result = try divide 10.0 0.0 with | :? System.DivideByZeroException as ex -> printfn "Error: %s" ex.Message 0.0
Railway-Oriented Programming:
// Chaining operations that might fail let validateAge age = if age >= 0 && age <= 150 then Ok age else Error "Age out of range" let validateName name = if String.IsNullOrWhiteSpace(name) then Error "Name cannot be empty" else Ok name // Compose validations type Person = { Name: string; Age: int } let createPerson name age = match validateName name, validateAge age with | Ok n, Ok a -> Ok { Name = n; Age = a } | Error e, _ -> Error e | _, Error e -> Error e // Or with Result.bind let createPerson2 name age = validateName name |> Result.bind (fun n -> validateAge age |> Result.map (fun a -> { Name = n; Age = a }))
Why this approach:
makes errors explicit in the type systemResult<'T, 'E>- Errors are values, not control flow
- Railway-oriented programming chains operations cleanly
- Compiler enforces error handling
Concurrency Patterns
Python asyncio → F# Async Workflows
Python:
import asyncio async def fetch_data(url: str) -> str: await asyncio.sleep(1) # Simulate I/O return f"Data from {url}" async def process_urls(urls: list[str]) -> list[str]: tasks = [fetch_data(url) for url in urls] results = await asyncio.gather(*tasks) return results # Run urls = ["url1", "url2", "url3"] results = asyncio.run(process_urls(urls))
F#:
open System // Async workflow let fetchData url = async { do! Async.Sleep 1000 // Simulate I/O return sprintf "Data from %s" url } let processUrls urls = async { let! results = urls |> List.map fetchData |> Async.Parallel return results } // Run let urls = ["url1"; "url2"; "url3"] let results = processUrls urls |> Async.RunSynchronously
Why this translation:
- F#
is a computation expression (similar to async/await)async { }
is likedo!
without a return valueawait
is likelet!
with a return valueawait
is likeAsync.Parallelasyncio.gather
Threading Models
| Python | F# | Notes |
|---|---|---|
| | Direct .NET interop |
| | Async workflows |
| - | F# uses .NET Task Parallel Library |
| | .NET Tasks |
F# Task vs Async:
// F# Async (native) let fetchAsync url = async { do! Async.Sleep 1000 return "data" } // .NET Task (for interop) open System.Threading.Tasks let fetchTask url = task { do! Task.Delay 1000 return "data" } // Convert between them let asyncToTask = fetchAsync "url" |> Async.StartAsTask let taskToAsync = fetchTask "url" |> Async.AwaitTask
Memory & Garbage Collection
Both Python and F# use garbage collection, making this conversion simpler than Python → Rust.
| Aspect | Python | F# |
|---|---|---|
| Memory management | Reference counting + GC | .NET GC (generational) |
| Mutability | Mutable by default | Immutable by default |
| String interning | Yes | Yes (.NET) |
| Object lifetime | GC managed | GC managed |
Key difference: F# defaults to immutability, which reduces bugs and makes concurrency safer.
Common Pitfalls
-
Mutability assumptions: Python is mutable by default; F# is immutable by default
- Use
keyword ormutable
/ResizeArray
for mutable stateDictionary
- Use
-
List performance: F#
is a linked list, not an arrayList<'T>- Use
(.NETResizeArray<'T>
) for random accessList<T> - Use
for fixed-size collectionsArray
- Use
-
Integer overflow: Python
never overflows; F#int
is 32-bitint- Use
for arbitrary precisionbigint - Use
context for overflow detectionChecked
- Use
-
String indexing: Python uses 0-based indexing; F# strings are .NET strings
- F# strings are UTF-16 (not UTF-8 like Python 3)
- Indexing:
gets as.[0]
, not a stringchar
-
Null values: Python has
; F# discouragesNonenull- Use
instead of nullable referencesOption<'T> - Only .NET interop types can be
null
- Use
-
Whitespace significance: Python uses indentation; F# uses indentation but less strictly
- F# requires proper indentation in computation expressions
- Use
to disable (not recommended)#light "off"
-
Function application: Python uses
; F# usesf(x, y)f x y- Parentheses only needed for grouping:
f (x + 1) y - Tupled arguments:
is a single tuple argumentf(x, y)
- Parentheses only needed for grouping:
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| dotnet CLI | Build, run, test F# projects | |
| Ionide | F# support for VS Code | Syntax, IntelliSense, debugging |
| JetBrains Rider | Full-featured F# IDE | Commercial, cross-platform |
| FSI (F# Interactive) | REPL for F# | Interactive development like Python REPL |
| Paket | Alternative package manager | More control than NuGet |
| FAKE | F# build automation | Like Make/Rake but in F# |
| FsCheck | Property-based testing | Like Python's Hypothesis |
| Expecto | F# test framework | Lightweight, functional |
| Fable.Python | F# → Python transpiler | Compile F# to Python |
| FSharp.Data | Type providers for CSV/JSON/XML | Strongly-typed data access |
Examples
Example 1: Simple - List Processing
Before (Python):
def filter_and_square(numbers: list[int]) -> list[int]: """Filter even numbers and square them.""" return [x * x for x in numbers if x % 2 == 0] result = filter_and_square([1, 2, 3, 4, 5, 6]) print(result) # [4, 16, 36]
After (F#):
// Type-inferred function let filterAndSquare numbers = numbers |> List.filter (fun x -> x % 2 = 0) |> List.map (fun x -> x * x) let result = filterAndSquare [1; 2; 3; 4; 5; 6] printfn "%A" result // [4; 16; 36]
Example 2: Medium - Error Handling + Options
Before (Python):
from typing import Optional def find_user(user_id: int, users: list[dict]) -> Optional[dict]: """Find user by ID.""" for user in users: if user["id"] == user_id: return user return None def get_user_name(user_id: int, users: list[dict]) -> str: """Get user name, or 'Unknown' if not found.""" user = find_user(user_id, users) if user: return user["name"] else: return "Unknown" users = [ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"} ] print(get_user_name(1, users)) # Alice print(get_user_name(99, users)) # Unknown
After (F#):
// Define types type User = { Id: int; Name: string } // Find user (returns Option) let findUser userId users = users |> List.tryFind (fun user -> user.Id = userId) // Get user name with default let getUserName userId users = findUser userId users |> Option.map (fun user -> user.Name) |> Option.defaultValue "Unknown" // Data let users = [ { Id = 1; Name = "Alice" } { Id = 2; Name = "Bob" } ] printfn "%s" (getUserName 1 users) // Alice printfn "%s" (getUserName 99 users) // Unknown
Example 3: Complex - Async Processing with Error Handling
Before (Python):
import asyncio from typing import Union, List from dataclasses import dataclass @dataclass class User: id: int name: str email: str async def fetch_user(user_id: int) -> Union[User, str]: """Fetch user asynchronously. Returns User or error message.""" await asyncio.sleep(0.1) # Simulate network delay if user_id < 0: return "Invalid user ID" elif user_id > 1000: return "User not found" else: return User( id=user_id, name=f"User{user_id}", email=f"user{user_id}@example.com" ) async def process_users(user_ids: List[int]) -> tuple[List[User], List[str]]: """Process multiple users, separating successes and errors.""" tasks = [fetch_user(uid) for uid in user_ids] results = await asyncio.gather(*tasks) users = [] errors = [] for result in results: if isinstance(result, User): users.append(result) else: errors.append(result) return users, errors # Run user_ids = [1, -5, 42, 9999] users, errors = asyncio.run(process_users(user_ids)) print(f"Fetched {len(users)} users") for user in users: print(f" - {user.name}: {user.email}") print(f"Encountered {len(errors)} errors") for error in errors: print(f" - {error}")
After (F#):
open System // Define types type User = { Id: int Name: string Email: string } type FetchError = | InvalidUserId | UserNotFound // Async function returning Result let fetchUser userId = async { do! Async.Sleep 100 // Simulate network delay if userId < 0 then return Error InvalidUserId elif userId > 1000 then return Error UserNotFound else return Ok { Id = userId Name = sprintf "User%d" userId Email = sprintf "user%d@example.com" userId } } // Process multiple users let processUsers userIds = async { let! results = userIds |> List.map fetchUser |> Async.Parallel let users, errors = results |> Array.partition (function Ok _ -> true | Error _ -> false) |> fun (oks, errs) -> let users = oks |> Array.choose (function Ok u -> Some u | _ -> None) let errors = errs |> Array.choose (function Error e -> Some e | _ -> None) (users, errors) return (users, errors) } // Run let userIds = [1; -5; 42; 9999] let users, errors = processUsers userIds |> Async.RunSynchronously printfn "Fetched %d users" (Array.length users) for user in users do printfn " - %s: %s" user.Name user.Email printfn "Encountered %d errors" (Array.length errors) for error in errors do let msg = match error with | InvalidUserId -> "Invalid user ID" | UserNotFound -> "User not found" printfn " - %s" msg
Key differences:
- F# uses
instead ofResult<'T, 'E>
for error handlingUnion[T, str] - Discriminated unions for error types (not string messages)
- Pattern matching with
for exhaustive handlingmatch
for concurrent operationsAsync.Parallel- Pipe operator chains for data transformations
- Type inference removes most type annotations
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Python → Rust conversion (similar dynamic → static transition)convert-python-rust
- TypeScript → Python (reverse static → dynamic)convert-typescript-python
- Python development patternslang-python-dev
- F# development patternslang-fsharp-dev
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- Async, channels, threads across languagespatterns-concurrency-dev
- JSON, validation, struct tags across languagespatterns-serialization-dev
- Decorators, macros, annotations across languagespatterns-metaprogramming-dev
External resources:
- F# for Python programmers - Official F# learning resources
- Fable.Python - F# to Python transpiler (reverse direction)
- F# Language Reference - Comprehensive F# documentation