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:
- Explicit
Mallo(...)arguments - Environment variables
- 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:
Server reload(use_reloaderinapp.run(...))Browser auto-refresh(live_reloadinMallo(...))
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'forapp.url_for(...)middleware=[...]route-specific middleware listcsrf=Falseto 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 responsedict/list-> JSON response withapplication/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/deletefetchone(sql, params=None)returns one row as dict orNonefetchall(sql, params=None)returns list of dict rowstransaction()context managerclose()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:
before_requesthooks- global middleware chain (
@app.middleware,app.use) - route-specific middleware (
middleware=[...]or group middleware) - route handler
after_requesthooks
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, port8000, debugon. - On Windows,
mallo runprints a gunicorn support warning. Usepython app.py. mallo createreturns 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:
- Framework core
- middleware capabilities
- routing enhancements
- session backend improvements
- Data and persistence
- migration tooling
- better SQLAlchemy utilities
- database testing coverage
- Developer experience
- CLI ergonomics
- clearer error diagnostics
- docs and examples
- Quality and stability
- regression tests
- edge-case handling
- compatibility verification
Future Improvements
Planned next improvements:
- Database migration commands in CLI (
mallo db init/migrate/upgrade) - Persistent session backends (Redis/file/cookie strategy)
- Route-scoped rate limits and cache policy
- Pluggable template engines (e.g., Jinja adapter)
- Production observability (request IDs, JSON logs, metrics hooks)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mallo-0.1.2.tar.gz.
File metadata
- Download URL: mallo-0.1.2.tar.gz
- Upload date:
- Size: 37.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
561b02b85a4ddf5eb35098c759a0052cd3623db530922bd20657a3cb65caa56c
|
|
| MD5 |
81f9f23b6dc48c05d3b2f3c7a1601d8d
|
|
| BLAKE2b-256 |
47e4898a93bfcc5fecc4131ad44e483c93957b3701fc38fa60ad078c65ae184a
|
Provenance
The following attestation bundles were made for mallo-0.1.2.tar.gz:
Publisher:
publish.yml on Betrand-dev/mallo-fr
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mallo-0.1.2.tar.gz -
Subject digest:
561b02b85a4ddf5eb35098c759a0052cd3623db530922bd20657a3cb65caa56c - Sigstore transparency entry: 996923350
- Sigstore integration time:
-
Permalink:
Betrand-dev/mallo-fr@ab53c89d519940ba29f5453ab8a5896c633ddacc -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/Betrand-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ab53c89d519940ba29f5453ab8a5896c633ddacc -
Trigger Event:
push
-
Statement type:
File details
Details for the file mallo-0.1.2-py3-none-any.whl.
File metadata
- Download URL: mallo-0.1.2-py3-none-any.whl
- Upload date:
- Size: 39.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59253438cd9bb277bc80d02410a8b8eb3bcd20d1749f0abd081afb2baab15505
|
|
| MD5 |
17bf72a0f66740c0b7458910302472c0
|
|
| BLAKE2b-256 |
c34c7192d23f7c136a7c1ea7adae5c22c179e235d9bfb1fdf2a799ae53f019dd
|
Provenance
The following attestation bundles were made for mallo-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on Betrand-dev/mallo-fr
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mallo-0.1.2-py3-none-any.whl -
Subject digest:
59253438cd9bb277bc80d02410a8b8eb3bcd20d1749f0abd081afb2baab15505 - Sigstore transparency entry: 996923408
- Sigstore integration time:
-
Permalink:
Betrand-dev/mallo-fr@ab53c89d519940ba29f5453ab8a5896c633ddacc -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/Betrand-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ab53c89d519940ba29f5453ab8a5896c633ddacc -
Trigger Event:
push
-
Statement type: