The Problem I Was Solving
I run a laser cutting business. Every day I need to:
- Process images for engraving
- Generate reports from production data
- Share tools with my team (who don't code)
I wanted something insane: Just type hints → complete web UI
So I built it.
Introducing FuncToWeb
After a month, I shipped FuncToWeb - a framework that auto-generates production-ready web interfaces from Python type hints.
Zero HTML. Zero CSS. Zero JavaScript.
The 30-Second Demo
from typing import Annotated
from func_to_web import run
from func_to_web.types import ImageFile
from pydantic import Field
def process_image(
image: ImageFile,
brightness: Annotated[int, Field(ge=0, le=100)] = 50,
mode: Literal["grayscale", "sepia", "blur"] = "grayscale"
):
# Your image processing logic here
processed = apply_filter(image, brightness, mode)
return processed # PIL Image auto-displayed
run(process_image)
What you get:
- ✅ Professional file upload with drag-and-drop
- ✅ Slider for brightness (0-100, validated client + server)
- ✅ Dropdown for mode selection
- ✅ Real-time validation errors
- ✅ Result image displayed beautifully
- ✅ Dark mode toggle (preference saved)
3 lines of setup. 0 lines of UI code.
Every Type Becomes The Right UI Component
The magic is in the type hints:
from func_to_web import run
from func_to_web.types import Color, Email, DocumentFile, ImageFile
from datetime import date, time
def create_project(
# Text types
name: str, # Text input
email: Email, # Email input with validation
# Numbers
budget: float, # Number input (decimals)
team_size: int, # Number input (integers)
# Choices
active: bool, # Checkbox
priority: Literal["low", "medium", "high"], # Dropdown
# Dates & Times
deadline: date, # Date picker
meeting: time, # Time picker
# Visual
brand_color: Color, # Color picker
# Files
logo: ImageFile, # Image upload (jpg, png, webp, etc.)
contract: DocumentFile, # Document upload (pdf, docx, etc.)
):
return f"Project '{name}' created!"
run(create_project)
All of this becomes a complete form automatically.
Validation That Actually Works
Pydantic Integration
from typing import Annotated
from func_to_web import run
from pydantic import Field
def register_user(
username: Annotated[str, Field(min_length=3, max_length=20)],
age: Annotated[int, Field(ge=18, le=120)],
email: Annotated[str, Field(pattern=r'^[^@]+@[^@]+\.[^@]+$')],
password: Annotated[str, Field(min_length=8)],
):
return f"Welcome {username}!"
run(register_user)
Error messages appear in real-time:
- "Username must be at least 3 characters"
- "Age must be between 18 and 120"
- "Please enter a valid email"
- "Password must be at least 8 characters"
Both client-side (instant) AND server-side (secure).
Lists With Two-Level Validation
This is where it gets crazy. You can validate both the list size AND each individual item:
from typing import Annotated
from func_to_web import run
from pydantic import Field
def rate_products(
# List must have 3-10 items
# Each item must be between 1-5
ratings: Annotated[
list[Annotated[int, Field(ge=1, le=5)]],
Field(min_length=3, max_length=10)
],
):
avg = sum(ratings) / len(ratings)
return f"Average rating: {avg:.2f}"
run(rate_products)
The UI shows:
- Dynamic add/remove buttons
- Validation error if you have < 3 or > 10 ratings
- Validation error if any rating is not 1-5
- Pre-filled defaults if you provide them
Works with ALL types: list[str], list[Color], list[date], etc.
Dynamic Dropdowns (Runtime-Populated)
Want dropdowns populated from a database or API? Use Literal[function]:
from func_to_web import run
from typing import Literal
def get_users_from_database():
# This runs when the form loads
return ["Alice", "Bob", "Charlie", "Diana"]
def assign_task(
user: Literal[get_users_from_database], # Dropdown calls function!
priority: Literal["low", "medium", "high"],
task: str
):
return f"Task '{task}' assigned to {user}"
run(assign_task)
The dropdown is populated at runtime by calling your function. Perfect for database queries, API calls, or dynamic options.
Optional Fields With State Control
Standard Python optional syntax works, but you can also control the initial state:
from func_to_web import run
from func_to_web.types import OptionalEnabled, OptionalDisabled, Email
def create_user(
username: str, # Required (no toggle)
# Standard Python: Auto-detected state
age: int | None = None, # Disabled (no default)
city: str | None = "Madrid", # Enabled (has default)
# Explicit control: You decide initial state
email: Email | OptionalEnabled, # Always starts enabled
phone: str | OptionalDisabled, # Always starts disabled
bio: str | OptionalDisabled = "Dev", # Disabled despite having default
):
return f"User {username} created"
run(create_user)
Each optional field gets a toggle switch. Disabled fields send None automatically.
Return Images and Plots (Auto-Displayed)
from func_to_web import run
from PIL import Image, ImageEnhance
from func_to_web.types import ImageFile
def enhance_image(
image: ImageFile,
brightness: float = 1.0,
contrast: float = 1.0,
):
img = Image.open(image)
# Enhance
if brightness != 1.0:
enhancer = ImageEnhance.Brightness(img)
img = enhancer.enhance(brightness)
if contrast != 1.0:
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(contrast)
return img # PIL Image auto-displayed in UI!
run(enhance_image)
Works with:
- PIL/Pillow Images (any format)
- Matplotlib figures
- Multiple images (returns list)
File Downloads
from func_to_web import run
from func_to_web.types import FileResponse
import pandas as pd
from io import BytesIO
def generate_report(
name: str,
items: int,
):
# Create Excel file
df = pd.DataFrame({"Name": [name] * items, "ID": range(items)})
buffer = BytesIO()
df.to_excel(buffer, index=False)
# Return as downloadable file
return FileResponse(
data=buffer.getvalue(),
filename=f"report_{name}.xlsx"
)
run(generate_report)
Users get a download button. Works with:
- Excel, CSV, JSON
- PDFs, Word documents
- ZIP files, images
- Any binary data
Return a list for multiple files: [FileResponse(...), FileResponse(...)]
Multiple Functions in One App
from func_to_web import run
def calculate_bmi(weight_kg: float, height_m: float):
"""Calculate Body Mass Index"""
bmi = weight_kg / (height_m ** 2)
return f"BMI: {bmi:.2f}"
def celsius_to_fahrenheit(celsius: float):
"""Convert Celsius to Fahrenheit"""
return f"{celsius}°C = {(celsius * 9/5) + 32}°F"
def reverse_text(text: str):
"""Reverse a string"""
return text[::-1]
# Pass a list - gets auto-generated index page
run([calculate_bmi, celsius_to_fahrenheit, reverse_text])
FuncToWeb creates a responsive index page where users pick which tool to use.
How It Works (Architecture)
-
Type Introspection - Python's
inspectmodule extracts function signature -
Pydantic Integration - Validates constraints from
Fieldannotations - Form Generation - Jinja2 templates convert types → HTML inputs
- FastAPI Backend - Handles requests, validation, file uploads
- Result Processing - Auto-detects return type (text, image, file) and displays accordingly
Tech Stack:
- FastAPI (async backend)
- Pydantic (validation)
- Vanilla JS/CSS (no React/Vue needed!)
- Jinja2 (templating)
- Uvicorn (ASGI server)
Total size: ~2000 lines of Python, 454 automated tests.
Real-World Use Cases
I use FuncToWeb daily in my laser cutting business:
- Image Processing - Brightness/contrast tools for engraving prep
- Production Reports - Generate Excel reports from laser job data
- Team Tools - Share scripts with non-technical operators
- Client Demos - Quick prototypes without deployment hassle
- Admin Panels - Manage products, orders, settings
Other users:
- Data scientists - Share analysis tools with stakeholders
- Backend devs - Internal tools without learning frontend
- Researchers - Share computational tools with colleagues
- DevOps - Quick dashboards for infrastructure tasks
Comparison: FuncToWeb vs Alternatives
| Feature | FuncToWeb | Streamlit | Gradio |
|---|---|---|---|
| Syntax | Pure type hints |
st. API |
gr.Interface() |
| Learning curve | Zero (if you know typing) | Medium | Low-Medium |
| Wrap existing functions | Works as-is | Need modifications | Need wrapper |
| Two-level list validation | ✅ Yes | ❌ No | ❌ No |
| Dynamic Literal[func] | ✅ Yes | ❌ No | ❌ No |
| Optional state control | ✅ Yes | Manual | Manual |
| Pydantic Field integration | ✅ Full | ❌ No | ❌ Limited |
| File type hints |
ImageFile, DocumentFile
|
Manual handling | gr.File() |
| Multiple functions | Auto index page | Multi-page app | Tabs interface |
| Dependencies | Minimal | Heavy | Medium |
Getting Started (60 Seconds)
pip install func-to-web
from func_to_web import run
from typing import Annotated
from pydantic import Field
def greet(
name: Annotated[str, Field(min_length=2)],
age: Annotated[int, Field(ge=0, le=150)],
formal: bool = False
):
greeting = "Good day" if formal else "Hey"
return f"{greeting} {name}! You are {age} years old."
run(greet)
Open http://localhost:8000 → Instant web UI! 🚀
Documentation
- Docs: functoweb.com
- GitHub: github.com/offerrall/FuncToWeb
- PyPI: pypi.org/project/func-to-web
- Examples: 20+ complete examples
Currently at 208 GitHub stars and growing fast.
Drop a comment with your use case!
And if you like the project, a ⭐ on GitHub would mean the world.
Resources
- Documentation - Complete guide with examples
- GitHub Repo - Source code + 20+ examples
-
PyPI Package -
pip install func-to-web
Top comments (0)