Skip to main content

A lightweight web framework with hot reload

Project description

Mallo

Lightweight Customizable Python web framework with a small core, built-in live reload, templates, routing, static files, and customizable error pages.

Installation

Install from PyPI:

pip install mallo

Recommended after install try mallo cli:

mallo create myapp
cd myapp
python app.py

Quick Start

Create app.py:

from mallo import Mallo

app = Mallo(__name__, live_reload=True)

@app.get('/')
def home(request):
    return '<h1>Hello from Mallo</h1>'

if __name__ == '__main__':
    app.run(debug=True, use_reloader=True)

Run:

python app.py

Open http://localhost:8000.


Config Object and Env Overrides

Mallo now resolves app settings through MalloConfig (defaults + environment + constructor overrides).

from mallo import Mallo

app = Mallo(
    __name__,
    template_folder='views',
    static_folder='assets',
    debug=True,
)

Environment variables (examples):

MALLO_DEBUG=1
MALLO_TEMPLATE_FOLDER=views
MALLO_STATIC_FOLDER=assets
MALLO_CSRF_PROTECT=0
MALLO_SECURITY_HEADERS=1

Precedence:

  1. Explicit Mallo(...) arguments
  2. Environment variables
  3. Framework defaults

Read resolved config:

print(app.config_obj.get('template_folder'))
print(app.config_obj.as_dict())

Hot Reload

Mallo has two reload-related behaviors:

  1. Server reload (use_reloader in app.run(...))
  2. Browser auto-refresh (live_reload in Mallo(...))

What the hot reloader does:

  • Watches your project files for changes (.py, .html, .css, .js, and others).
  • When a change is detected, it stops the running app process and starts a new one.
  • In debug mode with live_reload=True, HTML responses include a small script that checks a reload endpoint.
  • If the app process restarts, the browser detects a token change and refreshes automatically.

Enable both:

app = Mallo(__name__, live_reload=True)
app.run(debug=True, use_reloader=True)

When hot reloader is deactivated:

app = Mallo(__name__, live_reload=False)
app.run(debug=True, use_reloader=False)

What this means:

  • use_reloader=False: Python process will not restart on file changes.
  • live_reload=False: Browser auto-refresh script will not be injected.
  • You need to restart the app manually after code changes.

Template Rendering

Mallo supports 2 ways to render templates.

1) render_template() (from templates/ folder)

Use when your templates live in a templates directory.

from mallo import Mallo, render_template

app = Mallo(__name__)

@app.get('/')
def home(request):
    return render_template('index.html', name='Mallo')

Expected structure:

project/
  app.py
  templates/
    index.html

You can change template folder:

app = Mallo(__name__, template_folder='views')

2) render_template_file() (explicit file path)

Use when you want to render by full/relative file path directly.

from mallo import Mallo, render_template_file

app = Mallo(__name__)

@app.get('/about')
def about(request):
    return render_template_file('pages/about.html', title='About')

Template syntax

  • Variables: {{ name }}
  • Safe output (skip escaping): {{ html_content | safe }}
  • If blocks: {% if show %}...{% endif %}
  • For blocks: {% for item in items %}...{% endfor %}

Routing

General route decorator:

@app.route('/about')
def about(request):
    return 'About page'

@app.route() defaults to GET.

Route with multiple methods:

@app.route('/contact', methods=['GET', 'POST'])
def contact(request):
    if request.method == 'POST':
        return 'Form submitted'
    return 'Contact form'

Method shortcuts:

@app.get('/hello/<name>')
def hello(request, name):
    return f'Hello {name}'

@app.get('/user/<int:id>')
def user(request, id):
    return f'User #{id}'

@app.get('/file/<path:filepath>')
def file_route(request, filepath):
    return filepath

Generate routes by handler name:

profile_url = app.url_for('user', id=10)  # /user/10

Route Prefix Groups

Group routes under a shared prefix and middleware/default options:

api = app.group('/api')

@api.get('/users')
def users(request):
    return {'ok': True}

With group middleware:

def api_mw(request, call_next):
    response = call_next(request)
    response.headers['X-API'] = '1'
    return response

api = app.group('/api', middleware=[api_mw])

Per-Route Config

Routes can define specific options:

@app.post('/webhook', csrf=False)
def webhook(request):
    return 'ok'

Available per-route options:

  • name='route_name' for app.url_for(...)
  • middleware=[...] route-specific middleware list
  • csrf=False to disable CSRF for that route

Example:

def audit_mw(request, call_next):
    response = call_next(request)
    response.headers['X-Audit'] = 'on'
    return response

@app.get('/profile/<int:id>', name='profile_show', middleware=[audit_mw])
def profile(request, id):
    return f'profile {id}'

url = app.url_for('profile_show', id=10)  # /profile/10

Request Data

@app.get('/search')
def search(request):
    q = request.get('q', '')
    return f'query={q}'

@app.post('/submit')
def submit(request):
    name = request.post('name', '')
    return f'name={name}'

JSON body:

@app.post('/api')
def api(request):
    data = request.json or {}
    return {'received': data}

Multipart/form-data files:

from mallo.response import Response

@app.post('/upload')
def upload(request):
    file_info = request.files.get('file')
    if not file_info:
        return Response('No file', status=400)
    return f"Uploaded: {file_info['filename']}"

Responses

Return simple values:

  • str -> HTML response
  • dict / list -> JSON response with application/json

Or use response classes:

from mallo.response import JSONResponse, RedirectResponse, FileResponse

@app.get('/json')
def json_route(request):
    return JSONResponse({'ok': True})

@app.get('/go')
def go(request):
    return RedirectResponse('/')

Sessions and CSRF

Enable sessions by setting secret_key.

app = Mallo(__name__, secret_key='change-this-in-production')

Use session:

@app.get('/set')
def set_session(request):
    request.session['name'] = 'Betrand'
    return 'saved'

For unsafe methods (POST, PUT, DELETE), CSRF token is validated by default. Include token in forms:

@app.get('/form')
def form(request):
    return f"""
    <form method="post" action="/form">
      <input type="hidden" name="csrf_token" value="{request.csrf_token}">
      <input name="name">
      <button type="submit">Send</button>
    </form>
    """

Or send token in header X-Csrf-Token.


Database (SQLAlchemy Core)

Mallo now supports database integration via SQLAlchemy Core.

from mallo import Mallo, Database

app = Mallo(__name__)
db = Database("sqlite:///app.db")
app.init_db(db)  # request.db is now available in handlers

Basic operations:

@app.post('/users')
def create_user(request):
    request.db.execute(
        "INSERT INTO users (name) VALUES (:name)",
        {"name": "Betrand"}
    )
    return {"ok": True}

@app.get('/users')
def list_users(request):
    rows = request.db.fetchall("SELECT id, name FROM users ORDER BY id DESC")
    return {"users": rows}

Available DB methods:

  • execute(sql, params=None) for write/update/delete
  • fetchone(sql, params=None) returns one row as dict or None
  • fetchall(sql, params=None) returns list of dict rows
  • transaction() context manager
  • close() to dispose the engine

Transaction example:

from sqlalchemy import text

with db.transaction() as conn:
    conn.execute(
        text("INSERT INTO logs (message) VALUES (:msg)"),
        {"msg": "started"}
    )

See runnable example:

  • example/db_demo.py

Static Files

If static/ exists, files are served at /static/....

project/
  static/
    styles.css

In HTML:

<link rel="stylesheet" href="/static/styles.css">

Error Pages (404/500)

Mallo ships with default styled error pages.

Option 1: Configure custom error HTML files

app = Mallo(
    __name__,
    error_page_404='templates/errors/404.html',
    error_page_500='templates/errors/500.html',
)

Option 2: Register custom handlers

from mallo import Mallo, render_template

app = Mallo(__name__)

@app.errorhandler(404)
def not_found(request):
    return render_template('errors/404.html')

@app.errorhandler(500)
def server_error(request):
    return render_template('errors/500.html')

Custom handler takes precedence over error_page_404 / error_page_500.

Debug-mode errors are also shown using a friendly error screen:

  • clear error type + message
  • traceback hidden behind an expandable details block
  • non-overwhelming layout for faster debugging

Request Hooks

@app.before_request
def before(request):
    request.trace_id = 'abc123'

@app.after_request
def after(request, response):
    response.headers['X-Trace-Id'] = request.trace_id
    return response

Middleware

Mallo supports middleware with this signature:

middleware(request, call_next) -> Response|str|dict|list

Decorator style:

@app.middleware
def timing_middleware(request, call_next):
    response = call_next(request)
    response.headers['X-App'] = 'Mallo'
    return response

Function style:

def auth_middleware(request, call_next):
    if request.path.startswith('/admin'):
        return 'Unauthorized'
    return call_next(request)

app.use(auth_middleware)

Execution order:

  1. before_request hooks
  2. global middleware chain (@app.middleware, app.use)
  3. route-specific middleware (middleware=[...] or group middleware)
  4. route handler
  5. after_request hooks

Developer Diagnostics

In debug mode, Mallo shows startup diagnostics when no user routes are registered.

This helps catch common mistakes early, such as:

  • missing @ before route decorators
  • route module not imported before app.run()
  • route code path not executed

CLI

Create project:

mallo create myapp

Create with custom folders:

mallo create myapp --template-folder views --static-folder assets

Overwrite scaffold files if needed:

mallo create myapp --force

Run with gunicorn (Linux/macOS):

mallo run app:app

Options:

mallo run app:app --host 0.0.0.0 --port 9000 --no-debug --no-reload

Notes:

  • Defaults: host localhost, port 8000, debug on.
  • On Windows, mallo run prints a gunicorn support warning. Use python app.py.
  • mallo create returns clear errors for invalid project names, non-empty folders, and file overwrite conflicts.

Minimal Full Example

from mallo import Mallo, render_template

app = Mallo(__name__, secret_key='dev-secret', live_reload=True)

@app.get('/')
def home(request):
    return render_template('index.html', csrf_token=request.csrf_token, name='Mallo')

@app.post('/save')
def save(request):
    name = request.post('name', '')
    request.session['name'] = name
    return f'Saved: {name}'

@app.errorhandler(404)
def custom_404(request):
    return render_template('errors/404.html')

if __name__ == '__main__':
    app.run(debug=True, use_reloader=True)

Contributing

Repository:

https://github.com/Betrand-dev/mallo-fr.git

Contributions are welcome. Good contribution paths:

  1. Framework core
  • middleware capabilities
  • routing enhancements
  • session backend improvements
  1. Data and persistence
  • migration tooling
  • better SQLAlchemy utilities
  • database testing coverage
  1. Developer experience
  • CLI ergonomics
  • clearer error diagnostics
  • docs and examples
  1. Quality and stability
  • regression tests
  • edge-case handling
  • compatibility verification

Future Improvements

Planned next improvements:

  1. Database migration commands in CLI (mallo db init/migrate/upgrade)
  2. Persistent session backends (Redis/file/cookie strategy)
  3. Route-scoped rate limits and cache policy
  4. Pluggable template engines (e.g., Jinja adapter)
  5. Production observability (request IDs, JSON logs, metrics hooks)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mallo-0.1.0.tar.gz (35.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mallo-0.1.0-py3-none-any.whl (36.9 kB view details)

Uploaded Python 3

File details

Details for the file mallo-0.1.0.tar.gz.

File metadata

  • Download URL: mallo-0.1.0.tar.gz
  • Upload date:
  • Size: 35.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mallo-0.1.0.tar.gz
Algorithm Hash digest
SHA256 579dd73ee9b13c3753c6db1b634eb71725c285caa401ee8765904df0c1e64657
MD5 287c549f3fab015de885951509ff9d09
BLAKE2b-256 5cf14162e0953ecb4a28e40fe6e3ae4e7c8605e9c4ab638dbf67f168d7fb4ef9

See more details on using hashes here.

Provenance

The following attestation bundles were made for mallo-0.1.0.tar.gz:

Publisher: publish.yml on Betrand-dev/mallo-fr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mallo-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: mallo-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 36.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mallo-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e2001fada5992e7b1462d96b997cd510dae44f632c46554b0c6a79e4f988a46f
MD5 4377880658de86142c20fff40ac2b072
BLAKE2b-256 89b7fe6df0773786259a115866d8892e338408d7a2d5bba95cc369883c70e34f

See more details on using hashes here.

Provenance

The following attestation bundles were made for mallo-0.1.0-py3-none-any.whl:

Publisher: publish.yml on Betrand-dev/mallo-fr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page