# Emmett: a Python web framework that shows the whole request in one file

> Emmett is a full-stack Python web framework built around asyncio and ASGI, with its own ORM, its own templating and a routing layer that keeps the decorator chain readable. The example on its front page is a complete JSON API in thirty lines, which is either the pitch or the warning, depending on what you expect from a framework.

**emmett-framework/emmett** — The web framework for inventors

- Repository: https://github.com/emmett-framework/emmett
- Stars: 1,239 · Forks: 74
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/emmett-framework-emmett

## The front page example is the entire pitch

Emmett describes itself as a full-stack Python web framework designed with simplicity in mind, and the aim is stated plainly: to be clearly understandable, easy to be learned and to be used, so you can focus completely on your product's features.

The example that follows makes the case by being short. Here is the whole application: a model, a database, an authentication check, and a paginated JSON endpoint.

```python
from emmett import App, request, response
from emmett.orm import Database, Model, Field
from emmett.tools import service, requires

class Task(Model):
    name = Field.string()
    is_completed = Field.bool(default=False)

app = App(__name__)
app.config.db.uri = "postgres://user:password@localhost/foo"
db = Database(app)
db.define_models(Task)
app.pipeline = [db.pipe]

def is_authenticated():
    return request.headers.get("api-key") == "foobar"
    
def not_authorized():
    response.status = 401
    return {'error': 'not authorized'}

@app.route(methods='get')
@requires(is_authenticated, otherwise=not_authorized)
@service.json
async def todo():
    page = request.query_params.page or 1
    tasks = Task.where(
        lambda t: t.is_completed == False
    ).select(paginate=(page, 20))
    return {'tasks': tasks}
```

Nothing here is elided. The model definition, the database wiring, the pipeline hook, the auth guard, the JSON serialisation decorator and the paginated query are all present.

The dependency list in pyproject.toml is what makes this possible. Emmett pulls in click, emmett-core with the granian extra, emmett-pydal at a pinned 17.3.3, pendulum, pyyaml, renoir and severus. The ORM is pyDAL and the server extra is granian, so a single install brings up routing, database access and a server without you choosing them.

The trade is obvious once you see the list. Emmett does not use an ORM you already know, and it does not use a template engine you already know.

## Decorators do the work, and the order of them is your API contract

The route handler in the example carries three decorators, and the way they compose is the framework's central idea.

The innermost is @service.json, which declares that this handler returns JSON rather than a rendered template. The middle is @requires(is_authenticated, otherwise=not_authorized), a guard that runs a predicate and, when it fails, calls the alternative handler instead. The outer is @app.route(methods='get'), which registers the coroutine for that path and method.

Because the handler is async, the entire request path is coroutine based, and the framework is built on asyncio and ASGI. The topic list records both asgi and asyncio alongside rsgi, and the classifiers in pyproject.toml list Framework:: AsyncIO.

The guard mechanism is worth a second look because it removes the need for a decorator that only wraps the body. The check function reads request.headers directly and returns a boolean. The alternative handler sets response.status = 401 and returns a dict. Because that handler is plain, the framework does not need to know anything about your error format, and the same pattern works for a session check, a role check or a rate limit.

What you give up is the usual escape hatch of inspecting the request object after the fact. The request and response objects are module level imports rather than arguments, which keeps handler signatures short. Testing strategy is where that trade usually surfaces first.

## An ORM with pyDAL under the hood, not around it

The model definition in the example is a class with class attributes, which is pyDAL's style rather than SQLAlchemy's declarative style.

```python
class Task(Model):
    name = Field.string()
    is_completed = Field.bool(default=False)
```

The fields are typed with Field.string() and Field.bool, and the second one carries a default of False. The database is then attached to the app, the models are defined against it, and db.pipe is added to app.pipeline so database access is available inside request handlers.

The query style in the example is the other thing to internalise. Task.where takes a lambda that receives the table object, and the result is chained into .select with a paginate tuple of page and size. There is no query builder object, and no session or unit of work. The lambda is evaluated against a table that pyDAL translates into SQL.

The practical difference from SQLAlchemy is that the query is expressed as a Python expression over the table rather than as a chain of method calls on a query object. That reads well for short filters and gets awkward once you need joins, subqueries or anything that escapes the lambda. pyDAL is a mature library with a long history, and the Emmett dependency on it is pinned to an exact version, 17.3.3, which tells you the framework expects a specific pyDAL behaviour.

The app.pipeline mechanism is the second piece of architecture worth noting. It is a list that handlers depend on, and db.pipe is how the database context enters the request. A framework that exposes this as a list rather than hiding it makes the request lifecycle legible, at the cost of one more concept to learn.

## The dependency set is small, and that is a design choice

Look at the runtime dependencies again: click, emmett-core with granian, emmett-pydal, pendulum, pyyaml, renoir, severus. Seven entries, one of which is the framework's own core, and one of which is the ORM.

That is a small surface for a full-stack framework, and it explains why the example can be so short. You are not assembling a router, an ORM, a template engine, a serialiser and a server. Emmett already made those choices, so the cost of the framework is that you accept them.

The optional dependency extras show the same restraint. orjson and rapidjson are alternatives for JSON handling, and rloop and uvloop are alternative event loop implementations, both marked as not available on win32, and uvloop additionally restricted to CPython. These are the kind of choices that are cheap to offer and annoying to maintain, and their presence tells you the performance-conscious path exists without being the default.

The build system is hatchling, and the project version is 2.8.2 with a single author listed, Giovanni Barillari, whose GitHub handle gi0baro appears in the funding URL. A single-maintainer framework with a small dependency list is a coherent package, and it also means the bus factor question is a real one for anyone planning to depend on it.

The repository root adds the other signals: a Makefile whose default goal runs format, lint and test in sequence, a CHANGES.md for the changelog, a BACKERS.md, and a docs/ directory whose contents mirror the published documentation site, plus an examples/ directory holding the bloggy example that the tutorial describes.

## Semantic versioning with a stated deprecation window

The README states that Emmett is production ready and is compatible with Python 3.9 and above versions, and it also commits to semantic versioning with a major.minor.patch scheme, where major versions might introduce breaking changes, minor versions usually introduce new features and might introduce deprecations, and patch versions only introduce bug fixes.

The deprecation policy is the part that matters for a team. Deprecations are kept in place for at least three minor versions, and the drop is always communicated in the upgrade guide at emmett.sh/docs/latest/upgrading. A three-minor-version window is short by the standards of a mature library, where two to three years is more typical, so a team on a slow upgrade cadence should plan around it rather than be surprised by it.

There is a discrepancy worth flagging before anyone tries it. The README says the framework is compatible with Python 3.9 and above, but the packaging metadata in pyproject.toml sets requires-python to 3.10 or newer, and the classifiers list only Python 3.10 through 3.14. The install will refuse on 3.9. Both statements are in the project's own files, and the packaging metadata is the one that decides at install time.

The release history supports the claim of activity. The recent tags are Emmett 2.8.2 on 2026-10-08, Emmett 2.8.1 on 2026-04-07 and Emmett 2.8.0, named Inoue, on 2026-03-29. The version scheme also gives releases a name, which is a friendly touch, and the gap between 2.8.0 and 2.8.1 and 2.8.2 shows patch work continuing after a minor bump. The last push was on 2026-10-08, so the tree is current.

## BSD 3-Clause, with two carve-outs you have to read

Emmett is released under the BSD License, and the packaging metadata names BSD-3-Clause with an OSI Approved:: BSD License classifier.

The carve-out is stated in the README and is easy to skim past. Due to original license limitations, the contents under the validators and libs directories are included in Emmett under their original licenses, and the README tells you to check the source code for more details. Those two directories, emmett/validators and emmett/libs, are therefore not BSD.

That is the sort of detail that decides a licence review. A permissive framework with two subtrees under different terms is fine, but the terms of those subtrees need reading, and a downstream project that assumes everything under the emmett package is BSD could be wrong.

The BSD 3-Clause itself is a permissive licence with a short notice requirement and no copyleft. For a company evaluating Emmett for an internal service, the framework's own terms are not the obstacle; the two carve-out directories are the part to check first, and the README points you at the source rather than summarising them.

## Conclusion

Emmett suits a developer who wants one dependency-light package that covers routing, templating, ORM and an API response format without assembling a stack of five libraries, and who is comfortable reading a framework's source to understand its behaviour. It is the wrong tool if you need an ecosystem, a hiring pool or battle-tested integrations, and it is the wrong choice if you are stuck on Python 3.9, because the packaging metadata requires Python 3.10 or newer even though the README says 3.9 and above. Before you adopt it, read the upgrade guide linked from the README, since Emmett states that major versions may introduce breaking changes, and check whether the validators and libs subdirectories carry licence terms that differ from the BSD terms of the framework itself.

## FAQ

### How do I install Emmett?

The README does not give an install command. The project ships as the emmett package on PyPI, built with hatchling, and the documentation at emmett.sh/docs is the place the project points you to for setup. The package also provides an emmett console script mapped to emmett.cli:main.

### What Python versions does Emmett support?

The README states Emmett is compatible with Python 3.9 and above, but the packaging metadata sets requires-python to 3.10 or newer and lists classifiers for Python 3.10 through 3.14, so an install on 3.9 is refused.

### Which ORM does Emmett use?

pyDAL. The dependency is pinned as emmett-pydal==17.3.3, and models are defined with a Model class and Field attributes, with queries written as lambdas passed to Model.where and chained into .select.

### How does Emmett handle authentication in a route?

With the @requires decorator from emmett.tools, passing a predicate and an otherwise handler. If the predicate returns false, the otherwise handler runs, which in the README example sets response.status to 401 and returns an error dict.

### Is Emmett safe to upgrade across minor versions?

Emmett follows semantic versioning, so minor versions usually add features and may add deprecations while patch versions only fix bugs. The README states deprecations are kept for at least three minor versions and that removals are always communicated in the upgrade guide.

## Sources

- [emmett-framework/emmett on GitHub](https://github.com/emmett-framework/emmett)
- [Issues](https://github.com/emmett-framework/emmett/issues)
- [License: BSD-3-Clause](https://github.com/emmett-framework/emmett/blob/master/LICENSE)
- [README](https://github.com/emmett-framework/emmett/blob/master/README.md)
- [Releases](https://github.com/emmett-framework/emmett/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/emmett-framework-emmett
