From 18a82bc3b0d453d42f0b5749b4a9d77b150a261b Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 22 Dec 2025 08:45:29 +0100 Subject: [PATCH 1/3] chore: Added FastAPI use case --- .editorconfig | 1 + .vscode/settings.json | 5 ++ Fable.Literate/App.fs | 15 +++- Fable.Literate/Fable.Literate.fsproj | 2 +- README.md | 5 +- chapters/AsyncProgramming.fs | 8 +- chapters/FastAPI.fs | 128 +++++++++++++++++++++------ chapters/Introduction.fs | 2 +- chapters/Python.fs | 5 +- 9 files changed, 132 insertions(+), 39 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6f74c81..b228e14 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,3 +4,4 @@ root = true fsharp_multiline_bracket_style = stroustrup fsharp_record_multiline_formatter = number_of_items fsharp_max_record_number_of_items = 1 +fsharp_max_infix_operator_expression = 60 diff --git a/.vscode/settings.json b/.vscode/settings.json index 46de6ed..53894eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "aiohttp", "alfonsogarciacaro", + "boto", "Buildalyzer", "coro", "destructures", @@ -19,9 +20,13 @@ "nativeint", "ncave", "pathlib", + "pdfplumber", + "Plotly", "pyfile", "pyname", + "Pythonistas", "Pyxpecto", + "scikit", "Sergey", "stroustrup", "Tihon", diff --git a/Fable.Literate/App.fs b/Fable.Literate/App.fs index de70896..f9a5bb7 100644 --- a/Fable.Literate/App.fs +++ b/Fable.Literate/App.fs @@ -100,7 +100,9 @@ module Utils = if m.Value.Length = 1 then m.Value.ToLowerInvariant() else - m.Value.Substring(0, 1) + "_" + m.Value.Substring(1, 1).ToLowerInvariant() + m.Value.Substring(0, 1) + + "_" + + m.Value.Substring(1, 1).ToLowerInvariant() ) else name @@ -328,7 +330,10 @@ module MarkdownPrinter = | IncludePython symbols -> // Unresolved - should have been transformed let symbolList = String.concat ", " symbols - "\n\n" + + "\n\n" | Hidden _ -> "" // Should have been filtered /// Render a document to markdown string. @@ -348,7 +353,11 @@ module MarkdownPrinter = | s when s.StartsWith "#" -> inCodeBlock, ("#" + line) :: acc | _ -> inCodeBlock, line :: acc - lines |> Array.fold folder (false, []) |> snd |> List.rev |> String.concat "\n" + lines + |> Array.fold folder (false, []) + |> snd + |> List.rev + |> String.concat "\n" (** ## Pipeline Module diff --git a/Fable.Literate/Fable.Literate.fsproj b/Fable.Literate/Fable.Literate.fsproj index 5022a0a..bb33b37 100644 --- a/Fable.Literate/Fable.Literate.fsproj +++ b/Fable.Literate/Fable.Literate.fsproj @@ -8,7 +8,7 @@ - + diff --git a/README.md b/README.md index 78db4c6..5c66420 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Write F#, run Python - a practical guide to Fable.Python -This is a comprehensive guide to [Fable.Python](https://github.com/fable-compiler/Fable.Python/), written as literate F# that transpiles to Python and generates its own documentation. +This blog-post is a guide to [Fable.Python](https://github.com/fable-compiler/Fable.Python/), written as literate F# that transpiles to Python and generates its own documentation. ## Chapters @@ -23,9 +23,10 @@ This is a comprehensive guide to [Fable.Python](https://github.com/fable-compile ## The Strange Loop -This guide is self-documenting: each chapter is an `.fs` file with embedded Markdown comments. **Fabletext** (the final chapter) processes these files to generate the documentation you're reading - including itself. +This guide is self-documenting: each chapter is an `.fs` file with embedded Markdown comments. **Fable.Literate** (the final chapter) processes these files to generate the documentation you're reading - including itself. The chain: + 1. Write F# with embedded Markdown (`chapters/*.fs`) 2. Compile to Python with Fable 3. Run Fabletext (F# compiled to Python) to extract documentation diff --git a/chapters/AsyncProgramming.fs b/chapters/AsyncProgramming.fs index a1a01ec..cb2edee 100644 --- a/chapters/AsyncProgramming.fs +++ b/chapters/AsyncProgramming.fs @@ -76,7 +76,9 @@ F# async shines when composing multiple operations: let fetchMultipleAsync () = async { - let! results = [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] |> Async.Parallel + let! results = + [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] + |> Async.Parallel return results |> Array.toList } @@ -398,7 +400,9 @@ When you need rich composition primitives: let complexWorkflow () = async { // Run three operations in parallel - let! results = [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] |> Async.Parallel + let! results = + [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] + |> Async.Parallel // Then do something sequential do! Async.Sleep 100 diff --git a/chapters/FastAPI.fs b/chapters/FastAPI.fs index 3ea8962..eca8144 100644 --- a/chapters/FastAPI.fs +++ b/chapters/FastAPI.fs @@ -97,27 +97,35 @@ let items = ResizeArray() type API() = /// GET /items - List all items [] - static member get_items() : ResizeArray = - items + static member get_items() : ResizeArray = items /// GET /items/{item_id} - Get item by ID [] - static member get_item(item_id: int) : Task = task { - match items |> Seq.tryFind (fun i -> i.Id = item_id) with - | Some item -> return item :> obj - | None -> return {| error = "Item not found" |} - } + static member get_item(item_id: int) : Task = + task { + match items |> Seq.tryFind (fun i -> i.Id = item_id) with + | Some item -> return item :> obj + | None -> return {| error = "Item not found" |} + } /// POST /items - Create a new item [] - static member create_item(request: CreateItemRequest) : Task = task { - let newId = - if items.Count = 0 then 1 - else (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1 - let newItem = Item(newId, request.Name, request.Price, request.InStock) - items.Add(newItem) - return {| status = "created"; item = newItem |} - } + static member create_item(request: CreateItemRequest) : Task = + task { + let newId = + if items.Count = 0 then + 1 + else + (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1 + + let newItem = Item(newId, request.Name, request.Price, request.InStock) + items.Add(newItem) + + return {| + status = "created" + item = newItem + |} + } (** This generates Python with proper FastAPI decorators: @@ -143,8 +151,10 @@ F# anonymous records compile to Python dictionaries, perfect for JSON responses: [] type HealthAPI() = [] - static member health() = - {| status = "healthy"; version = "1.0.0" |} + static member health() = {| + status = "healthy" + version = "1.0.0" + |} (** ## Async Endpoints @@ -155,11 +165,12 @@ For I/O-bound operations, use `task { }` to create async endpoints: [] type AsyncAPI() = [] - static member slow_operation() = task { - // Simulate async work (e.g., database query) - do! Task.Delay(100) - return {| message = "Done!" |} - } + static member slow_operation() = + task { + // Simulate async work (e.g., database query) + do! Task.Delay(100) + return {| message = "Done!" |} + } (** The `task { }` computation expression compiles to Python's `async def`, @@ -175,12 +186,16 @@ Path parameters are extracted from the URL: [] type UsersAPI() = [] - static member get_user(user_id: int) = - {| id = user_id; name = "User " + string user_id |} + static member get_user(user_id: int) = {| + id = user_id + name = "User " + string user_id + |} [] - static member get_user_post(user_id: int, post_id: int) = - {| user_id = user_id; post_id = post_id |} + static member get_user_post(user_id: int, post_id: int) = {| + user_id = user_id + post_id = post_id + |} (** ### Query Parameters @@ -191,8 +206,10 @@ Query parameters are function arguments not in the path: [] type SearchAPI() = [] - static member search(q: string, limit: int) = - {| query = q; limit = limit |} + static member search(q: string, limit: int) = {| + query = q + limit = limit + |} (** A request to `/search?q=hello&limit=10` maps to `search("hello", 10)`. @@ -213,7 +230,11 @@ type UserCrudAPI() = [] static member create_user(request: CreateUserRequest) = // FastAPI automatically validates the request body - {| status = "created"; name = request.name; email = request.email |} + {| + status = "created" + name = request.name + email = request.email + |} (** FastAPI validates the incoming JSON against the Pydantic model and returns @@ -230,8 +251,10 @@ type ErrorAPI() = static member protected_route() = // Check authentication (simplified example) let isAuthenticated = false + if not isAuthenticated then raise (System.Exception("Not authenticated")) + {| message = "Secret data" |} (** @@ -336,4 +359,51 @@ This combination gives you: 4. **Familiar ecosystem** - Deploy with standard Python tools You write type-safe F# code, but deploy and run it like any Python web service. + +## Hybrid Architecture: F# Backend with Python Endpoints + +Another compelling use case is when you have an existing web service written in F# (using +ASP.NET Core, Giraffe, or Oxpecker) but need access to the Python ecosystem for specific +functionality. You can use FastAPI to expose endpoints that leverage Python libraries, +while your main service remains in F#. + +This hybrid approach shines when you need: + +### AI/ML Libraries + +- **LangChain** / **LlamaIndex** - LLM orchestration and RAG pipelines +- **Hugging Face Transformers** - Pre-trained models for NLP, vision, audio +- **OpenAI SDK** / **Anthropic SDK** - LLM API integration with structured outputs +- **scikit-learn** - Classical machine learning models +- **PyTorch** / **TensorFlow** - Deep learning inference + +### Data Science & Analytics + +- **Pandas** / **Polars** - Data manipulation and analysis +- **NumPy** - Numerical computing +- **Matplotlib** / **Plotly** - Chart and visualization generation +- **Apache Arrow** - Efficient cross-language data interchange + +### Document Processing + +- **PyMuPDF** / **pdfplumber** - PDF text and table extraction +- **python-docx** - Word document generation +- **Pillow** - Image processing and manipulation +- **OpenCV** - Computer vision operations + +### Specialized APIs + +- **boto3** - AWS services (S3, Lambda, SQS, etc.) +- **google-cloud-*** - GCP services (BigQuery, Cloud Storage, Vertex AI) + +### Scientific Computing + +- **SciPy** - Scientific algorithms and optimization +- **SymPy** - Symbolic mathematics +- **NetworkX** - Graph algorithms and analysis + +The pattern is straightforward: your F# service handles core domain logic and type-safe +business rules, while specific endpoints delegate to a FastAPI service for capabilities +where Python dominates. This is especially powerful for AI/ML workloads where the Python +ecosystem is unmatched. *) diff --git a/chapters/Introduction.fs b/chapters/Introduction.fs index dfff1e1..382c4bf 100644 --- a/chapters/Introduction.fs +++ b/chapters/Introduction.fs @@ -21,7 +21,7 @@ a compiler that transforms F# code into Python. 7. [Testing](#heading-testing-fablepython-projects) - Using pytest with F# code 8. [Fable v5](#heading-fable-v5-whats-new) - New features and the Rust core 9. [Pydantic Integration](#heading-pydantic-interop) - Type-safe data validation -10. [FastAPI](#heading-fastapi) - Building type-safe web APIs +10. [FastAPI](#heading-fastapi) - Building type-safe web APIs in the Python ecosystem 11. [Units of Measure](#heading-units-of-measure) - Compile-time dimensional analysis 12. [Fable.Literate](#heading-fableliterate-the-strange-loop) - The tool that wrote this post diff --git a/chapters/Python.fs b/chapters/Python.fs index 7b70d90..2713f08 100644 --- a/chapters/Python.fs +++ b/chapters/Python.fs @@ -159,7 +159,10 @@ let numbers = [ -1; 2; -3; 4; 5 ] // F# pipeline - reads left to right, top to bottom let result = - numbers |> List.filter (fun x -> x > 0) |> List.map (fun x -> x * 2) |> List.sum + numbers + |> List.filter (fun x -> x > 0) + |> List.map (fun x -> x * 2) + |> List.sum (** The `|>` operator takes the value on the left and passes it as the last From ce2e9940222c53f5de8dc4c19c718b4d6aeaed61 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 22 Dec 2025 10:18:05 +0100 Subject: [PATCH 2/3] chore: Small improvements to the text --- CLAUDE.md | 2 +- chapters/Bindings.fs | 10 +++++++--- chapters/GettingStarted.fs | 8 +++++--- chapters/Introduction.fs | 5 +++-- chapters/Python.fs | 4 +++- justfile | 7 +++++++ 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8e06c25..9ece319 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,4 +95,4 @@ docs/ - [Fable.Python docs](https://fable.io/docs/getting-started/python.html) - [Fable.Python GitHub](https://github.com/fable-compiler/Fable.Python/) -- [Content Plan](CONTENT-PLAN.md) - Chapter structure and TODO items +- [Content Plan](CONTENT-PLAN.md) - Chapter structure and TODO items (important!) diff --git a/chapters/Bindings.fs b/chapters/Bindings.fs index 12abb02..4603a22 100644 --- a/chapters/Bindings.fs +++ b/chapters/Bindings.fs @@ -3,9 +3,13 @@ module Bindings (** # Creating Python Bindings -When a Python library doesn't have F# bindings, you can create your own. -This chapter covers the patterns and best practices for writing type-safe -bindings that feel natural in F#. +When a Python library doesn't have F# bindings, you can create your own. This chapter +covers the patterns and best practices for writing type-safe bindings that feel natural +in F#. + +> Writing bindings have long been a major pain point, spending countless hours wrestling +> with interop details. With AI-assisted coding tools, generating initial binding code +> for your favorite Python libraries has become much easier. ## Core Principles diff --git a/chapters/GettingStarted.fs b/chapters/GettingStarted.fs index 4800abd..dbbe5c9 100644 --- a/chapters/GettingStarted.fs +++ b/chapters/GettingStarted.fs @@ -12,7 +12,8 @@ You'll need: - [.NET SDK](https://dotnet.microsoft.com/download) (6.0 or later. We recommend installing the latest LTS version, currently .NET 10) - [Python 3.12+](https://www.python.org/downloads/) (Fable targets Python 3.12 or higher) -- [uv](https://docs.astral.sh/uv/) (recommended) - A fast Python package manager written in Rust +- [uv](https://docs.astral.sh/uv/) (recommended) - A fast Python package manager written in Rust that + simplifies dependency management, virtual environments, and the installation of Python itself. If you don't have `uv` installed: @@ -60,8 +61,9 @@ pip install "fable-library==5.0.0a21" --- -**Note:** Version pinning matters. The fable-library version must match -your Fable compiler version. PyPI uses `5.0.0a21` format instead of `5.0.0-alpha.21`. +**Note:** Version pinning matters. The fable-library version must match your Fable +compiler version. Note that PyPI uses `5.0.0a21` format instead of `5.0.0-alpha.21` for +prerelease alpha releases. --- diff --git a/chapters/Introduction.fs b/chapters/Introduction.fs index 382c4bf..be482d5 100644 --- a/chapters/Introduction.fs +++ b/chapters/Introduction.fs @@ -42,9 +42,10 @@ F# is a functional-first language with powerful features like: - **Type inference** - Write less, express more - **Pattern matching** - Elegant handling of complex data - **Immutability by default** - Safer, more predictable code -- **Algebraic data types** - Model your domain precisely +- **Algebraic data types** - Model your domain precisely with discriminated unions and records -With Fable.Python, you get all these benefits while targeting the Python ecosystem. +These features make F# excellent for [Domain Modeling](https://www.pragprog.com/titles/swdddf/domain-modeling-made-functional/) - +expressing business rules as types that the compiler enforces. Python is currently [the most popular programming language in the world](https://www.tiobe.com/tiobe-index/). And no matter what you think of Python, it will always be the second best language for everything. That ubiquity is exactly why diff --git a/chapters/Python.fs b/chapters/Python.fs index 2713f08..3e622f1 100644 --- a/chapters/Python.fs +++ b/chapters/Python.fs @@ -47,7 +47,7 @@ No need to write it unless you want to. ### Pattern Matching -Python 3.10+ has `match`/`case`. F# pattern matching is similar but more powerful: +Python 3.10+ has `match`/`case`: ```python # Python match/case @@ -59,6 +59,8 @@ match command: case _: return unknown_command() ``` + +F# pattern matching is similar but more powerful: *) let handleCommand command = diff --git a/justfile b/justfile index ffc82af..abd8f86 100644 --- a/justfile +++ b/justfile @@ -59,6 +59,9 @@ generate: build format-python blogpost: build format-python #!/usr/bin/env bash mkdir -p docs + # Get Fable version (strip ANSI codes) and timestamp + fable_version=$(dotnet fable --version 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' || echo "unknown") + timestamp=$(date -u +"%Y-%m-%d %H:%M UTC") first=true for name in {{chapters}}; do # Convert PascalCase to snake_case for Python file naming @@ -75,6 +78,10 @@ blogpost: build format-python uv run python output/Fable.Literate/app.py \ --python-file "$pyfile" \ "chapters/${name}.fs" > docs/blogpost.md + # Insert version banner after the first heading using awk + awk -v ts="$timestamp" -v fv="$fable_version" \ + 'NR==1 {print; print ""; print "*Generated on " ts " using Fable v" fv "*"; next} {print}' \ + docs/blogpost.md > docs/blogpost.tmp && mv docs/blogpost.tmp docs/blogpost.md first=false else # Remaining chapters get headers increased by one level From cc06c34ca6699cb8b84795e70bf1853b6153b841 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 22 Dec 2025 13:14:43 +0100 Subject: [PATCH 3/3] chore: cleanup language --- chapters/Bindings.fs | 3 +-- chapters/FastAPI.fs | 2 +- chapters/GettingStarted.fs | 6 +++--- chapters/Interop.fs | 9 ++++----- chapters/Introduction.fs | 6 +++--- chapters/Python.fs | 5 ++--- chapters/Summary.fs | 2 +- chapters/Testing.fs | 3 +++ 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/chapters/Bindings.fs b/chapters/Bindings.fs index 4603a22..251abdf 100644 --- a/chapters/Bindings.fs +++ b/chapters/Bindings.fs @@ -277,6 +277,5 @@ let createClient url = myLibrary.createClient url ## What's Next? -Now you know how to create bindings. The **Compatibility** chapter covers -which F# features work with Fable.Python and any limitations to be aware of. +With bindings covered, the **Compatibility** chapter shows which F# features work with Fable.Python and any limitations to be aware of. *) diff --git a/chapters/FastAPI.fs b/chapters/FastAPI.fs index eca8144..ea62714 100644 --- a/chapters/FastAPI.fs +++ b/chapters/FastAPI.fs @@ -367,7 +367,7 @@ ASP.NET Core, Giraffe, or Oxpecker) but need access to the Python ecosystem for functionality. You can use FastAPI to expose endpoints that leverage Python libraries, while your main service remains in F#. -This hybrid approach shines when you need: +This hybrid approach works well when you need: ### AI/ML Libraries diff --git a/chapters/GettingStarted.fs b/chapters/GettingStarted.fs index dbbe5c9..f3d4068 100644 --- a/chapters/GettingStarted.fs +++ b/chapters/GettingStarted.fs @@ -3,7 +3,7 @@ module GettingStarted (** # Getting Started with Fable.Python -Let's set up a Fable.Python project from scratch and get our first F# code running as Python. +In this section we will set up a Fable.Python project from scratch and get our first F# code running as Python. ## Prerequisites @@ -138,6 +138,6 @@ my-fable-python/ ## Next Steps -Now that you have a working setup, let's explore how to interact with Python -libraries in the next chapter on **Bindings**. +Now that you have a working setup, let's see how we can interact with Python +libraries by using **Bindings**. *) diff --git a/chapters/Interop.fs b/chapters/Interop.fs index ca19dfc..a99aaa2 100644 --- a/chapters/Interop.fs +++ b/chapters/Interop.fs @@ -3,8 +3,7 @@ module Interop (** # Python Interop -Now that you have a Fable.Python project set up, let's explore how to work -with Python libraries and the existing bindings in the Fable.Python ecosystem. +With a Fable.Python project set up, we can start to work with Python libraries and the existing bindings in the Fable.Python ecosystem. ## The Fable.Python Library @@ -427,7 +426,7 @@ let loadConfig (path: string) = (** ## What's Next? -Now you know how to use existing Python bindings and core interop features. -In the next chapter, we'll learn how to create your own bindings for -Python libraries that don't have F# bindings yet. +Now you know how to use existing Python bindings and core interop features. In the next +chapter we will see how you can create your own bindings for Python libraries that don't +have F# bindings yet. *) diff --git a/chapters/Introduction.fs b/chapters/Introduction.fs index be482d5..a51897f 100644 --- a/chapters/Introduction.fs +++ b/chapters/Introduction.fs @@ -7,7 +7,7 @@ module Introduction 2025](https://sergeytihon.com/2025/11/03/f-advent-calendar-in-english-2025/). Thank you, Sergey Tihon, for organizing this wonderful tradition that brings the F# community together every year! -Welcome to this guide on [Fable](https://fable.io/) and [Fable.Python](https://github.com/fable-compiler/Fable.Python/) - +This guide covers [Fable](https://fable.io/) and [Fable.Python](https://github.com/fable-compiler/Fable.Python/) - a compiler that transforms F# code into Python. ## Table of Contents @@ -75,7 +75,7 @@ Fable.Python is a great choice when: ## A First Example -Let's start with F# code that compiles to Python: +Let's begin with a simple F# example: *) let greet (name: string) = $"Hello, {name}!" @@ -118,6 +118,6 @@ shape variant, the compiler will warn you about unhandled cases in the `area` fu ## What's Next? -In the following chapters, we'll explore setting up your environment, working with Python libraries, and understanding +In the following chapters, we will get started by setting up your environment, working with Python libraries, and understanding F# compatibility with Fable. Let's begin. *) diff --git a/chapters/Python.fs b/chapters/Python.fs index 3e622f1..e42af88 100644 --- a/chapters/Python.fs +++ b/chapters/Python.fs @@ -3,8 +3,7 @@ module ForPythonDevelopers (** # Are You a Python Developer? -If you're coming from Python, welcome. This chapter will help you understand the F# code you'll see throughout this -guide. F# is more approachable than it might appear, and many concepts are familiar. +If you're coming from Python, this chapter covers the F# code you'll see throughout this guide. F# is more approachable than it might appear, and many concepts are familiar. ## What is F#? @@ -16,7 +15,7 @@ is the language you know. ## Key Concepts You'll See -Let's map F# concepts to Python equivalents you already understand. +Here's how F# concepts map to Python equivalents you already know. ### Type Inference diff --git a/chapters/Summary.fs b/chapters/Summary.fs index 3d9c834..48e82de 100644 --- a/chapters/Summary.fs +++ b/chapters/Summary.fs @@ -49,5 +49,5 @@ from the community make it better for everyone. - [Fable.Python on GitHub](https://github.com/fable-compiler/Fable.Python/) - [F# Software Foundation](https://fsharp.org/) -Welcome to Fable.Python. Now go build something. +... now go build something. *) diff --git a/chapters/Testing.fs b/chapters/Testing.fs index a004212..5f07fc1 100644 --- a/chapters/Testing.fs +++ b/chapters/Testing.fs @@ -235,6 +235,9 @@ project setup: ### Justfile Commands +We recommend [just](https://github.com/casey/just) as a command runner - it's like +`make` but simpler and cross-platform. Here's how to set up test commands: + ```just # Run tests (.NET) test: