Tina4 Python - Quick Reference#
๐ฅ Hot Tips
- Routes go in
src/routes/, templates insrc/templates/, static files insrc/public/ - GET routes are public by default; POST/PUT/PATCH/DELETE require a token
- Return a
dictfromresponse()and the framework setsapplication/json - Run
tina4 serveto start the dev server on port 7146
Installation โข Static Websites โข Routing โข Middleware โข Templates โข Sessions โข SCSS โข Environments โข Authentication โข Forms & Tokens โข AJAX โข OpenAPI โข Databases โข Database Results โข Migrations โข ORM โข CRUD โข REST Client โข Testing โข Services โข Websockets โข Queues โข WSDL โข GraphQL โข Localization โข HTML Builder โข Events โข Logging โข Cache โข Health โข DI Container โข Error Overlay โข Dev Admin โข CLI โข MCP โข FakeData
Installation#
# Install the tina4 CLI once. Windows: irm https://tina4.com/install.ps1 | iexcurl -fsSL https://tina4.com/install.sh | shโtina4 init python my-appcd my-apptina4 serveThe CLI scaffolds your project, installs the dependencies, and starts the server. No dependency tree. No version conflicts. Your browser opens to http://localhost:7146 and the welcome page greets you.
More details on project setup and customization.
Static Websites#
Put .twig files in ./src/templates and assets in ./src/public. The framework serves them without additional configuration.
<!-- src/templates/index.twig --><h1>Hello Static World</h1>More details on static website routing.
Basic Routing#
Import the route decorators from tina4_python. Each handler receives request and response. Path parameters arrive as function arguments.
from tina4_python import get, postโ@get("/")async def get_home(request, response): return response("<h1>Hello Tina4 Python</h1>")โ# POST requires a formToken in the body or Bearer auth@post("/api")async def post_api(request, response): return response({"data": request.body})โ# Redirect after a POST@post("/register")async def post_register(request, response): return response.redirect("/welcome")Follow the links for basic routing and dynamic routing with variables.
Middleware#
Middleware runs before and after your route handler. Define a class with static methods, then attach it with the @middleware decorator.
from tina4_python import get, middlewareโclass RunSomething:โ @staticmethod def before_something(request, response): response.content += "Before" return request, responseโ @staticmethod def after_something(request, response): response.content += "After" return request, responseโ @staticmethod def before_and_after_something(request, response): response.content += "[Before / After Something]" return request, responseโ@middleware(RunSomething)@get("/middleware")async def get_middleware(request, response): return response("Route") # Before[Before / After Something]Route[Before / After Something]AfterFollow the links for more on Middleware Declaration and Linking to Routes.
Template Rendering#
Put .twig files in ./src/templates and assets in ./src/public. The template engine reads your layout, fills in the variables, and delivers clean HTML.
<!-- src/templates/index.twig --><h1>Hello {{name}}</h1>from tina4_python import getโ@get("/")async def get_home(request, response): return response.render("index.twig", {"name": "World!"})Sessions#
The default session handler stores data on the file system. Override TINA4_SESSION_BACKEND in .env to switch backends.
The value is the backend name, not a class name, and it is normalised: leading or trailing spaces and capitals are fine, so Redis resolves.
An unrecognised value RAISES at startup, naming the bad value and the valid ones. It used to fall back to the file backend silently, which meant a typo produced a running app writing sessions to local disk while you believed they were in Redis: nothing logged, nothing failed, and the symptom only surfaced later as users being logged out whenever a request landed on another instance. Leaving TINA4_SESSION_BACKEND unset, or setting it to an empty value, still gives you the file default.
| Value | Backend | Required package |
|---|---|---|
file (default) | File system | -- |
redis | Redis | redis |
valkey | Valkey | valkey |
mongodb | MongoDB | pymongo |
database | Your ORM DB | -- |
TINA4_SESSION_BACKEND=mongodbTINA4_SESSION_MONGO_URI=mongodb://localhost:27017TINA4_SESSION_MONGO_DB=tina4_sessionsTINA4_SESSION_MONGO_COLLECTION=sessionsfrom tina4_python import getโ@get("/session/set")async def get_session_set(request, response): request.session.set("name", "Joe") request.session.set("info", {"info": ["one", "two", "three"]}) return response("Session Set!")โโ@get("/session/get")async def get_session_get(request, response): name = request.session.get("name") info = request.session.get("info") return response({"name": name, "info": info})โโ@get("/session/clear")async def get_session_clear(request, response): request.session.delete("name") return response("Session key removed!")SCSS Stylesheets#
Drop .scss files in ./src/scss. The framework compiles them to ./src/public/css.
// src/scss/main.scss$primary: #2c3e50;body { background: $primary; color: white;}More details on css and scss.
Environments#
The .env file holds your project configuration. The framework reads it at startup.
TINA4_DEBUG=trueTINA4_PORT=7145TINA4_DATABASE_URL=sqlite:///data/app.dbTINA4_LOG_LEVEL=ALLTINA4_API_KEY=ABC1234import osโapi_key = os.getenv("TINA4_API_KEY", "ABC1234")Access env vars programmatically:
from tina4_python.dotenv import load_env, get_env, has_env, require_env, is_truthyโload_env() # Load .env file (auto on server start)get_env("TINA4_DATABASE_URL") # Get value or Noneget_env("PORT", "7145") # Get value with defaulthas_env("TINA4_DEBUG") # True if setrequire_env("TINA4_DATABASE_URL") # Raises if missingis_truthy(get_env("TINA4_DEBUG")) # True for "true", "1", "yes"Authentication#
POST, PUT, PATCH, and DELETE routes require a Bearer token by default. Pass Authorization: Bearer TINA4_API_KEY in the request header. Use @noauth() to open a route to everyone. Use @secured() to lock a GET route behind authentication.
from tina4_python import get, post, noauth, securedfrom tina4_python.auth import Authโ@post("/login")@noauth()async def login(request, response): token = Auth.get_token({"user_id": 1, "role": "admin"}) return response({"token": token})โ@get("/protected")@secured()async def secret(request, response): return response("Welcome!")โ@get("/verify")async def verify(request, response): token = request.headers.get("Authorization", "").replace("Bearer ", "") payload = Auth.valid_token(token) return response({"valid": payload is not None})HTML Forms and Tokens#
<form method="POST" action="/register"> {{ ("Register" ~ RANDOM()) | form_token }} <input name="email"> <button>Save</button></form>More details on posting form data, basic form handling, how to generate form tokens, dealing with file uploads, returning errors, disabling route auth and a full login example.
AJAX and frond.js#
Tina4 ships with frond.js, a small zero-dependency JavaScript library for AJAX calls, form submissions, and real-time WebSocket connections.
More details on available features.
OpenAPI and Swagger UI#
Visit http://localhost:7145/swagger. Decorated routes appear in the Swagger UI without manual annotation.
from tina4_python import getfrom tina4_python.swagger import descriptionโ@get("/users")@description("Get all users")async def users(request, response): return response(User().select("*"))Follow the links for more on Configuration, Usage and Decorators.
Databases#
from tina4_python.database import Databaseโ# dba = Database("<driver>:<hostname>/<port>:database_name", username, password)dba = Database("sqlite3:data.db")The adapter speaks PostgreSQL, MySQL, and SQLite. It translates your queries into whichever dialect the database understands.
Follow the links for more on Available Connections, Core Methods, Usage and Full transaction control.
Database Results#
result = dba.fetch("select * from test_record order by id", limit=3, offset=1)โarray = result.to_array()paginated = result.to_paginate()csv_data = result.to_csv()json_data = result.to_json()Looking at detailed Usage will deepen your understanding.
Migrations#
tina4 migrate:create create_users_table-- migrations/00001_create_users_table.sqlCREATE TABLE users( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);tina4 migrateMigrations have limitations worth knowing before you use them at scale.
ORM#
from tina4_python.orm import ORM, IntegerField, StringFieldโclass User(ORM): id = IntegerField(primary_key=True, auto_increment=True) name = StringField()โUser({"name": "Alice"}).save()โuser = User()user.load("id = ?", [1])ORM covers more ground than this snippet shows. Study the Advanced Detail to get the full value.
CRUD#
from tina4_python import getโ@get("/users/dashboard")async def dashboard(request, response): users = User().select("id, name, email") return response.render("users/dashboard.twig", {"crud": users.to_crud(request)}){{ crud }}More details on how CRUD generates its files and where they live.
Consuming REST APIs#
from tina4_python import Apiโapi = Api("https://api.example.com", auth_header="Bearer xyz")result = api.get("/users/42")print(result["body"])More details on sending POST data, authorization headers, and other controls for outbound API requests.
Inline Testing#
from tina4_python import testsโ@tests( assert_equal((7, 7), 1), assert_equal((-1, 1), -1), assert_raises(ZeroDivisionError, (5, 0)),)def divide(a: int, b: int) -> float: if b == 0: raise ZeroDivisionError("division by zero") return a / bRun: tina4 test
Services#
Due to the nature of Python, services are not necessary.
Websockets#
WebSocket support is built in. No extra dependencies. Define a handler with the @websocket decorator, and the framework manages the connection alongside your HTTP routes on the same port.
from tina4_python import websocketโ@websocket("/ws/chat")async def chat_ws(connection, event, data): if event == "message": await connection.send(f"Echo: {data}")Have a look at the PubSub example under Websockets.
Queues#
Supports litequeue (default/SQLite), RabbitMQ, Kafka, and MongoDB backends. The queue system uses produce() and consume() directly, with no separate Producer or Consumer classes.
from tina4_python.queue import Queueโ# Produce a messagequeue = Queue(topic="emails")queue.produce("emails", {"to": "alice@example.com", "subject": "Welcome"})โ# Consume messagesfor job in queue.consume("emails"): print(job.payload)Full details on backend configuration, batching, multi-queue consumers, and error handling.
WSDL#
Subclass WSDL and decorate each operation with @wsdl_operation, giving the return shape. Drop the file in src/routes/ and the framework serves the SOAP endpoint plus its generated WSDL.
from typing import Listfrom tina4_python.wsdl import WSDL, wsdl_operationโโclass Calculator(WSDL):โ @wsdl_operation({"Result": int}) def Add(self, a: int, b: int): return {"Result": a + b}โ @wsdl_operation({"Numbers": List[int], "Total": int}) def SumList(self, Numbers: List[int]): return {"Numbers": Numbers, "Total": sum(Numbers)}More Details on WSDL configuration and usage.
GraphQL#
from tina4_python.graphql import GraphQLโschema = """type Query { hello(name: String!): String users: [User]}โtype User { id: Int name: String email: String}"""โresolvers = { "hello": lambda info, name: f"Hello, {name}!", "users": lambda info: db.fetch("SELECT * FROM users").records,}โgraphql = GraphQL(schema, resolvers)Register the endpoint:
from tina4_python import post, noauthโ@post("/graphql")@noauth()async def handle_graphql(request, response): result = graphql.execute(request.body.get("query", "")) return response(result)GraphiQL UI available at /__dev/graphql in debug mode.
Localization (i18n)#
Translation files live in src/locales/ as JSON. Create an I18n instance with a locale directory and a default locale, switch languages at runtime, and translate keys with t().
from tina4_python.i18n import I18nโi18n = I18n(locale_dir="src/locales", default_locale="en")โi18n.set_locale("af") # switch languagei18n.t("welcome_message") # translated string for the active localei18n.t("greeting", name="Ada") # with interpolationMissing keys fall back to the default locale.
HTML Builder#
from tina4_python.HtmlElement import HTMLElement, add_html_helpersโel = HTMLElement("div", {"class": "card"}, ["Hello"])str(el) # <div class="card">Hello</div>โ# Nestingpage = HTMLElement("div")( HTMLElement("h1")("Title"), HTMLElement("p")("Content"),)โ# Helper functionsadd_html_helpers(globals())html = _div({"class": "card"}, _h1("Title"), _p("Description"), _a({"href": "/more"}, "Read more"),)Events#
from tina4_python.core.events import on, emit, once, offโ@on("user.created")def send_welcome(user): print(f"Welcome {user['name']}!")โ@once("app.ready")def on_ready(): print("Started!")โemit("user.created", {"name": "Alice"})Logging#
from tina4_python.debug import LogโLog.info("Server started")Log.debug("Request received", path="/api/users")Log.warning("Slow query", duration_ms=450)Log.error("Connection failed", host="db.example.com")Set TINA4_LOG_LEVEL in .env: ALL, DEBUG, INFO, WARNING, ERROR.
Response Cache#
from tina4_python.core.router import get, cachedโ@cached(max_age=120)@get("/api/products")async def products(request, response): return response(expensive_query())Health Endpoint#
Built-in at /__health, with /health always registered too. Returns {"status": "ok", "version": "3.13.94", "uptime": 123.4, "framework": "tina4-python"}. Configure the path with TINA4_HEALTH_PATH. It is a liveness probe: it reports on the process, never on a database or cache.
DI Container#
from tina4_python.container import Containerโcontainer = Container()container.singleton("db", lambda: Database("sqlite:///app.db"))container.register("mailer", lambda: MailService())db = container.get("db")Error Overlay#
Automatic in debug mode. Shows syntax-highlighted stack trace with source context. Set TINA4_DEBUG=true in .env.
Dev Admin#
Available at /__dev in debug mode. Includes route inspector, database tab, request capture, metrics bubble chart, gallery examples, dev mailbox.
CLI Commands#
tina4 init python my-app # Scaffold projecttina4 serve # Start dev servertina4 serve --production # Production modetina4 doctor # Check environmenttina4 env # Configure .envtina4 docs # Download documentationtina4 generate model User # Generate scaffoldingtina4 migrate # Run migrationstina4 test # Run teststina4 ai # Install AI contextMCP Server#
Auto-starts on /__mcp in debug mode. Exposes 24 dev tools via JSON-RPC 2.0 over SSE. Works with Claude Code, Cursor, and other MCP clients.
FakeData#
from tina4_python.seeder import FakeDataโfake = FakeData()fake.name() # "Alice Johnson"fake.email() # "alice@example.com"fake.phone() # "+1-555-0123"fake.sentence() # "The quick brown fox..."fake.integer() # 4821๐ Download the book#
Tina4 for Python Developers (PDF): full reference, printable, with clickable table of contents and PDF outline. Regenerated with every release.