Skip to main content

A lightweight web framework with hot reload

Project description

Mallo

Lightweight 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

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.


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

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.


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.


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

CLI

Create project:

mallo create myapp

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.

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 for:

  • testing coverage
  • middleware improvements
  • session backends
  • template engine features
  • documentation and examples

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.3.0a5.tar.gz (27.3 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.3.0a5-py3-none-any.whl (30.9 kB view details)

Uploaded Python 3

File details

Details for the file mallo-0.3.0a5.tar.gz.

File metadata

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

File hashes

Hashes for mallo-0.3.0a5.tar.gz
Algorithm Hash digest
SHA256 19f4108b7a485b5a0c34ac9dcbf51361f9208fd6d02befb89b60b780f6c310fc
MD5 2e16daa563906f66d67a01ebebdde9bf
BLAKE2b-256 aa42e6869acfe722b84405cf1417d29f1c21a4dff75c2ca88752de05132a5445

See more details on using hashes here.

Provenance

The following attestation bundles were made for mallo-0.3.0a5.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.3.0a5-py3-none-any.whl.

File metadata

  • Download URL: mallo-0.3.0a5-py3-none-any.whl
  • Upload date:
  • Size: 30.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.3.0a5-py3-none-any.whl
Algorithm Hash digest
SHA256 d95590e4bb00c820a77a9d522891af4937cec400997df2c953038db291a4cf7b
MD5 47cfdfe1f40daf8d733a66ef70cb7c2b
BLAKE2b-256 cee3a3e0756a143b69f025ff96df4803cc87ef63b0e92727cceb86b92929391c

See more details on using hashes here.

Provenance

The following attestation bundles were made for mallo-0.3.0a5-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