Pyramid - незаслуженно забытый фреймворк
Я полез разбираться в Pyramid не из археологического интереса. Причина была вполне практической: мне нужно было понять, как правильно интегрировать с ним Jam. А когда пишешь интеграцию с web-фреймворком, поверхностного знания уровня "вот так объявляется endpoint" уже недостаточно. Нужно понять, где живёт request-local state, как устроен dispatch, где проходит граница между authentication и authorization, в какую точку request lifecycle можно встроиться и что сам фреймворк считает расширением.
И вот здесь Pyramid оказался неожиданно интересным. На первый взгляд это framework из другой эпохи: WSGI, Configurator, отдельные routes и views, registry, traversal, tweens, наследие Pylons и Zope Component Architecture. Но за этой терминологией находится довольно последовательная архитектура. Pyramid не пытается дать готовую модель приложения. Он скорее предоставляет набор механизмов, которые можно комбинировать: URL dispatch, traversal, predicate-based view lookup, application registry, security policy, events, request extensions, tweens и пользовательские configurator directives. Официальный слоган проекта - "Start Small, Finish Big, Stay Finished" - довольно точно передаёт эту философию.
Именно это я считаю главной причиной, почему на Pyramid интересно посмотреть даже в 2026 году. Не потому, что теперь всем следует бросить Starlette, Django или что-то ещё и переписывать проекты на Pyramid. А потому, что Pyramid напоминает: привычная сегодня модель path + HTTP method -> function далеко не единственный способ построить web-framework.
У Pyramid route не равен view. Найденный URL не обязательно непосредственно определяет handler. URL вообще может быть путём по дереву Python-объектов. View выбирается отдельно, с учётом context, имени, HTTP-метода и других predicates. Authorization тоже встроен именно в эту модель: разрешение проверяется уже после того, как Pyramid нашёл context и потенциальный view.
При этом "романтизировать" Pyramid я бы не стал. На текущий момент актуальная версия на PyPI - Pyramid 2.1, выпущенная 11 марта 2026 года; она требует Python 3.10+ и заявляет поддержку вплоть до Python 3.14. Но предыдущий bugfix-релиз, 2.0.2, был ещё 25 августа 2023 года. В январе 2026 года один из основных мейнтейнеров Michael Merickel прямо описывал положение проекта словами: "Pyramid isn't dead but it is severely lacking contributors", упоминая нехватку активных ревьюеров и людей, способных заниматься релизами. Релиз 2.1 через два месяца после этого обсуждения показывает, что проект действительно не мёртв, но риск маленькой maintainer base никуда автоматически не исчезает. Поэтому мой тезис в этой статье будет чуть уже заголовка: "Pyramid незаслуженно забыт как набор архитектурных идей. Но это не означает, что он автоматически является лучшим выбором для нового проекта в 2026 году."
Зачем я полез в Pyramid?
Всё началось с Jam.
Когда делаешь интеграцию auth*-библиотеки с очередным web-framework, довольно быстро выясняется, насколько мало о framework говорит его hello world. Для обычного приложения можно годами пользоваться только публичным happy path. Для интеграции этого недостаточно.
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
def hello_world(request):
return Response('Hello World!')
if __name__ == '__main__':
with Configurator() as config:
config.add_route('hello', '/')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()
server = make_server('0.0.0.0', 6543, app)
server.serve_forever()
Мне нужно было ответить на более неприятные вопросы:
- Где создаётся request?
- Где хранить объект интеграции, живущий столько же, сколько приложение?
- Где хранить состояние аутентификации одного запроса?
- Есть ли middleware и что именно framework считает middleware?
- Можно ли вычислять identity лениво?
- Есть ли нативный механизм authorization?
- Как framework выбирает handler?
- В какой момент handler уже выбран, но ещё не вызван?
- Как правильно добавлять framework-specific API, не monkey-patch'я всё подряд?
У Pyramid ответы почти на все эти вопросы оказались не случайными hooks, добавленными задним числом, а частью общей архитектуры. Configurator, application registry, add_request_method(), tweens, events, security policy и собственные directives существуют именно как extension points. Например, Pyramid прямо рекомендует add_request_method() как более composable альтернативу кастомному Request subclass для add-on'ов: функция может стать ленивым per-request свойством и кешироваться через reify=True. А Configurator можно расширять собственными directives, которые участвуют в той же системе configuration actions, conflict resolution и introspection, что и встроенные механизмы framework.
Но чтобы понять, почему Pyramid устроен именно так, полезно ненадолго вернуться в 2011 год. Pyramid не появился с нуля. Он возник в результате объединения Pylons и repoze.bfg. В официальном foreword к документации история сформулирована довольно прямо: Ben Bangert и Chris McDonough обнаружили, что Pylons и repoze.bfg покрывают почти одну и ту же область; проекты объединились, сохранили идентичность Pylons Project, а repoze.bfg предоставил техническую основу будущего framework. Pyramid 1.0 был подготовлен к PyCon 2011. Связь с Zope здесь тоже не случайна, но её легко пересказать слишком грубо. Pyramid — не переименованный Zope. Скорее через repoze.bfg он унаследовал ряд идей из мира Zope: resource traversal, component registry, adapters и вообще отношение к приложению как к композиции независимо регистрируемых компонентов. В foreword repoze.bfg даже описывается как способ пользоваться Zope-style idioms, не будучи обязанным строить приложение в стиле Zope; в современной документации Pyramid application registry по-прежнему прямо используется как registry для Zope Component Architecture utilities. Это происхождение многое объясняет.
Сегодня web-framework часто начинается с маршрутизатора:
@app.get("/users/{user_id}")
async def get_user(user_id: int):
...
Pyramid начинается с несколько другой идеи: Eсть registry приложения, есть набор configuration actions, есть request, есть механизм поиска context и есть отдельный механизм поиска подходящего view. Поэтому первая встреча с Pyramid может производить впечатление избыточности. После того как понимаешь происхождение этих механизмов, становится видно, что это не случайный boilerplate, а другая декомпозиция задачи.
Pyramid как toolbox: Configurator, routes, views и predicates
Меня немного раздражает термин "микрофреймвок", оксюморон же какой-то и вот Pyramid я бы так не называл. Официально сам проект называет Pyramid небольшим web-framework и много лет использует формулу Start Small, Finish Big. Но практически мне полезнее думать о нём как о toolbox framework: он предоставляет много primitives, однако довольно мало пытается решить за приложение, какую ORM, модель данных или архитектуру сервисного слоя ему использовать. Это существенное отличие от подхода, в котором вместе с HTTP API framework пытается предложить ещё и модель валидации, сериализации, dependency resolution и API schema generation. Лично мне это близко. Для web-слоя мне обычно достаточно framework primitives; для данных я скорее выберу dataclasses или msgspec, а dependency injection считаю отдельной архитектурной задачей. Поэтому из современных framework мне концептуально ближе Starlette: он довольно честно остаётся ASGI toolkit. Pyramid гораздо больше Starlette и устроен совершенно иначе, но желание не определять весь application stack за пользователя у них в чём-то похоже.
Configurator - хорошая точка, с которой видно эту философию, минимальное Pyramid-приложение выглядит так:
from pyramid.config import Configurator
from pyramid.response import Response
def hello_world(request):
return Response("Hello, World!")
with Configurator() as config:
config.add_route("hello", "/")
config.add_view(hello_world, route_name="hello")
app = config.make_wsgi_app()
Сначала здесь хочется задать очевидный вопрос: "Почему route и handler надо регистрировать отдельно?"
Но это как раз один из наиболее интересных design choices Pyramid.
Configurator здесь стоит воспринимать не просто как объект app, в который немедленно добавляются callback'и. Внутри Pyramid конфигурация во многом строится как набор actions. Они накапливаются и обрабатываются при config.commit() или, в обычном приложении, при make_wsgi_app(). У actions есть discriminators; благодаря этому система умеет обнаруживать и разрешать конфигурационные конфликты. Тот же механизм доступен сторонним расширениям. То есть я бы мысленно переводил:
config.add_route(...)
config.add_view(...)
не в "добавь callback в router", а в "опиши framework ещё один кусок конфигурации приложения". Именно поэтому вокруг Configurator можно построить полноценное стороннее расширение:
def includeme(config):
config.add_directive("add_something", add_something)
# после чего:
config.include("some_extension")
config.add_something(...)
Официальная документация прямо описывает это как предназначенный для framework extensions механизм: custom directive может вызывать другие directives, создавать actions, участвовать в conflict resolution и добавлять introspection metadata.
Для Jam это уже выглядит гораздо интереснее, чем "добавьте наш middleware руками".
Route в Pyramid - не view. Представим один URL:
config.add_route("user", "/users/{user_id}")
А затем несколько views:
config.add_view(
get_user,
route_name="user",
request_method="GET",
)
config.add_view(
update_user,
route_name="user",
request_method="PUT",
)
config.add_view(
delete_user,
route_name="user",
request_method="DELETE",
)
Route отвечает на вопрос примерно такого уровня: "Этот request вообще соответствует /users/{user_id}?"
После route match Pyramid сохраняет matchdict и matched_route, но на этом dispatch не заканчивается. Дальше framework ищет view, используя context, request, view name и набор predicates. И это важное различие.
В API, где декоратор объединяет path, method и callable,
@app.get("/users/{user_id}")
def get_user(...):
...
легко начать воспринимать всё это как одно понятие — endpoint. Pyramid сохраняет границы между ними: route matching -> context resolution -> view lookup -> view invocation
Отсюда естественным образом появляются view predicates. View можно ограничить по route_name, request_method, context, имени view, request parameter и другим характеристикам. accept позволяет учитывать Accept header, хотя документация отдельно отмечает, что технически этот аргумент устроен немного иначе и не является обычным predicate. Более специфичные регистрации проверяются раньше менее специфичных; если predicates конкретного view не совпали, Pyramid продолжает поиск следующего подходящего view.
Условно:
config.add_view(
render_user,
route_name="user",
request_method="GET",
)
config.add_view(
render_user_json,
route_name="user",
request_method="GET",
accept="application/json",
)
Это уже не просто "router нашёл функцию". Это маленькая dispatch system.
Ещё интереснее, что predicates расширяемы.
Pyramid предоставляет add_view_predicate() и add_route_predicate(). Predicate factory обычно получает значение при конфигурации, а затем объект predicate вызывается для конкретного request. Для view predicate сигнатура вызова — (context, request).
Например, упрощённый custom predicate мог бы выглядеть так:
class FeaturePredicate:
def __init__(self, value, info):
self.value = value
def text(self):
return f"feature = {self.value}"
phash = text
def __call__(self, context, request):
return self.value in request.features
# и далее:
config.add_view(
beta_dashboard,
route_name="dashboard",
feature="new-dashboard",
)
Сам интерфейс здесь немного старомодный: text, phash, callable class. Но архитектурно возможность добавлять новое измерение в сам алгоритм view lookup мне кажется интереснее очередного декоратора над handler.
При этом важно не использовать predicates вообще для всего. Например, делать authenticated=True custom predicate как основной механизм access control - плохая идея. Если predicates не дают найти view, естественным результатом view lookup является 404. А у Pyramid есть отдельная security system, где отказ в permission приводит к forbidden flow. То есть framework сам проводит полезную границу между "такого представления ресурса нет" и "представление есть, но тебе нельзя его вызвать".
Это как раз тот случай, когда сложность Pyramid начинает окупаться: несколько похожих на поверхности задач оказываются разными понятиями внутри фреймворка.
Traversal: URL как путь по объектной модели
Самая необычная часть Pyramid для разработчика, привыкшего к современным API-frameworks, - это, наверное, traversal. Обычно мы воспринимаем routing примерно так: /users/42 -> "/users/{id}" -> id = 42 -> handler. URL - строка, а роутер пытается найти паттерн, которому эта строка соответствует.
Pyramid умеет работать и так. Но у него есть другая модель: URL -> дерево объектов -> context -> view lookup -> view. Официальная документация описывает resource tree буквально как дерево dictionary-like Python objects. Pyramid получает root object, а затем последовательно использует __getitem__ для сегментов пути, пока не найдёт итоговый resource context. Найденный объект становится request.context и участвует в дальнейшем view lookup.
Например, концептуально приложение может иметь такое дерево:
Root
└── "projects" → Projects
├── "41" → Project(id=41)
├── "42" → Project(id=42)
└── "43" → Project(id=43)
Запрос: /projects/42. И это условно означает:
root["projects"]["42"]
и результатом traversal становится:
Project(id=42)
Этот объект - уже не просто набор строковых path parameters. Это context, относительно которого Pyramid теперь ищет подходящий view.
Схематично различие выглядит так: <тут картинку нарисовать>
При traversal view можно выбирать в том числе по типу context:
config.add_view(
project_view,
context=Project,
)
# или защитить конкретную операцию permission:
config.add_view(
edit_project,
context=Project,
name="edit",
permission="project.edit",
)
context и name являются частью механизма view lookup; context может быть классом или интерфейсом, который найденный resource должен реализовывать.
На первый взгляд всё это кажется очень непривычным. Но модель перестаёт выглядеть экзотикой, если URL действительно отражает иерархию domain resources: /organizations/acme/projects/compiler/files/src/main.py В классическом routing приходится отдельно разобрать:
organization = "acme"
project = "compiler"
path = "src/main.py"
а затем вручную восстановить связи между объектами.
Traversal предлагает другой взгляд: сам URL уже является инструкцией перемещения по resource graph.
Это не значит, что я бы строил так каждый JSON API. Для плоского набора CRUD endpoints обычный routing чаще понятнее. Но traversal показывает важную вещь: URL dispatch - частный случай resource location, а не фундаментальный закон web-framework architecture. Причём Pyramid не заставляет выбрать либо routing, либо traversal. Они могут комбинироваться. В request lifecycle Pyramid сначала может сопоставить route, затем получить root factory этого route, а после этого выполнить traversal. В документации view configuration прямо упоминается распространённая комбинация route с *traverse, когда часть URL выбирает route, а оставшийся path проходит по resource tree.
config.add_route(
"projects",
"/projects/*traverse",
factory=projects_root_factory,
)
Тогда /projects задаёт boundary подсистемы, а всё после него можно интерпретировать уже через object graph. И вот здесь разделение route и view начинает выглядеть уже не как лишний boilerplate, а как необходимое следствие более общей dispatch model.
Что происходит с запросом: lifecycle, tweens, registry и security
Для интеграции Jam мне особенно важно было понять не столько syntax Pyramid, сколько полный request lifecycle.
В официальной документации он описан довольно подробно.
WSGI server передаёт окружение Pyramid router. Router создаёт Request, кладёт request и application registry в свой current-context stack, отправляет NewRequest, выполняет route matching, затем BeforeTraversal, создаёт root, выполняет traversal, сохраняет context и view_name, отправляет ContextFound, ищет view, проверяет security permission, вызывает view, обрабатывает exception views, response callbacks и NewResponse, формирует WSGI response и в конце запускает finished callbacks.
Tweens, пожалуй, наиболее забавно названная часть framework.
Название происходит от between: tween находится между основным request handler Pyramid и верхним WSGI-слоем. Он похож на middleware, но получает уже Pyramid Request и имеет доступ к application registry и другим framework-specific сущностям.
Минимальный tween:
def timing_tween_factory(handler, registry):
def timing_tween(request):
before_request(request)
response = handler(request)
after_request(request, response)
return response
return timing_tween
Фабрика получает handler и registry, а созданный tween вызывается для каждого request. Pyramid отдельно предупреждает, что состояние экземпляра tween является общим между запросами, поэтому mutable per-request state там хранить не стоит.
Для интеграций это гораздо удобнее обычного WSGI middleware, когда тебе уже нужны именно Pyramid concepts.
Но tween не следует превращать в универсальный молоток. В Pyramid есть более узкие hooks:
NewRequest,BeforeTraversal,ContextFound,NewResponse eventsдля конкретных стадийlifecycle;response callbacksдля действий над успешно полученнымresponse;finished callbacksдля кода, который должен выполниться в самом конце даже приexception;add_request_method()дляper-request properties;response adaptersдля преобразования пользовательскогоreturn typeвIResponse.
Именно такое количество ортогональных extension points мне в Pyramid нравится больше всего. Framework не говорит: "у нас есть middleware, делайте в нём всё". Он пытается дать отдельный механизм для отдельного класса задач.
Application registry - второй важный кусок. Вместо глобального singleton Pyramid имеет registry, связанный с конкретным приложением. Внутри он основан на component-registry подходе Zope Component Architecture; туда можно регистрировать utilities, и официальная документация использует такой pattern при описании framework extensions.
При этом использовать все возможности ZCA совсем не обязательно. Для простого extension вполне допустима гораздо более прагматичная схема. В официальной документации по custom directives даже есть пример, где directive просто выполняет:
config.registry.jammyjam = jammyjam
во время configuration action. Забавное совпадение названия примера с Jam, но для моей задачи этот пример почти идеально показывает нужный pattern: long-lived integration object хранится в registry, а не в module-global variable.
Наконец, у Pyramid есть полноценный security hook.
В Pyramid 2.1 security не включена по умолчанию. Приложение может установить объект, реализующий ISecurityPolicy, через:
config.set_security_policy(policy)
У policy есть понятия identity, authenticated_userid, permits, remember и forget. Когда найденный view имеет permission, Pyramid перед его вызовом передаёт request, context и это permission в security policy. Policy возвращает Allowed или Denied.
То есть можно написать:
config.add_view(
update_project,
route_name="project",
request_method="PUT",
permission="project.update",
)
и authorization становится частью стандартного dispatch lifecycle, а не декоратором, который пользователь обязан не забыть повесить на handler.
Для Jam это практически готовая точка интеграции.
Pyramid в современном Python web: WSGI, ASGI, DX и сравнение со Starlette/FastAPI
Теперь плохая новость: вся эта архитектурная красота существует внутри фреймворка, который в 2026 году остаётся WSGI.
Актуальный Pyramid 2.1 на PyPI прямо классифицирован как WSGI, минимальный пример создаёт приложение через make_wsgi_app(), а официальное описание request processing начинается с передачи WSGI environment сервером в Pyramid router.
У Pyramid давно обсуждался asyncio. Ещё в 2018 году был открыт issue Pyramid 2.0 asyncio support #3391; в той дискуссии среди препятствий тогда упоминался WebOb и отсутствие async I/O support в используемом stack. Но эту старую дискуссию не стоит выдавать за актуальный roadmap или утверждать, что ровно тот же технический blocker существует сегодня. Важнее наблюдаемое состояние на 2026 год: стабильный Pyramid 2.1 всё ещё предоставляет WSGI application model, а не native ASGI request pipeline.
И это уже реальный trade-off.
Starlette сегодня официально позиционируется как lightweight ASGI framework/toolkit, ориентированный на async web services, WebSockets, modular middleware и mountable applications; среди его целей отдельно отмечаются небольшой уровень сложности и малое число обязательных зависимостей. FastAPI строит более opinionated API layer поверх этого стека: его собственная документация прямо говорит, что web parts основаны на Starlette, а data parts на Pydantic; поверх этого framework даёт dependency system, автоматическую валидацию и OpenAPI/interactive docs.
Я специально не хочу превращать это сравнение в "Pyramid хорош, FastAPI плох". FastAPI сам по себе плох, без сравнения с чем-либо.
У меня лично FastAPI не вызывает большого интереса именно потому, что значительная часть добавленной поверх Starlette ценности находится в областях, которые я предпочитаю решать иначе: Pydantic мне обычно не нужен, для структур данных мне ближе dataclasses или msgspec, а DI я не считаю обязанностью HTTP framework. Но это мои архитектурные предпочтения, а не аргумент, что встроенными возможностями FastAPI никто не пользуется.
В этом смысле Starlette для меня даже более полезная точка сравнения. Starlette это: маленький набор ASGI/web primitives + собирай остальной stack сам. А Pyramid скорее: богатый набор framework primitives + собирай архитектуру приложения сам То есть Pyramid не минималистичен по числу возможностей. Он минимально opinionated относительно того, какие из них должны определять архитектуру твоего приложения.
И это одновременно его сильная сторона и проблема так называемого developer experience.
Чтобы продуктивно использовать FastAPI, можно довольно долго вообще не знать, как устроен его внутренний dispatch. Чтобы по-настоящему оценить Pyramid, приходится довольно рано столкнуться с понятиями context, root factory, registry, view lookup, predicates и tweens. Это повышает conceptual overhead. С другой стороны, эти понятия дают гораздо больше пространства стороннему разработчику. Их наличие и роль подробно документированы в официальных главах по request processing, hooks, traversal и extension configuration.
С состоянием экосистемы всё тоже неоднозначно. Называть Pyramid мёртвым на сентябрь 2026 года просто неверно: Pyramid 2.1 вышел 11 марта 2026 года, поддерживает современные Python 3.10–3.14 и имеет актуальную документацию.
Но игнорировать maintainer risk тоже нельзя. В GitHub Discussion #3801 "Status of Pyramid (and other pylons projects)" 5 января 2026 года Michael Merickel написал, что framework серьёзно страдает от недостатка контрибьюторов, что релизы без его участия «languished», а release process стал слишком ручным и трудоёмким. Он также отмечал необходимость модернизации typing, packaging и project templates.
При этом в той же дискуссии появились люди, готовые помогать, а уже в феврале начались pre-releases 2.1 и 11 марта вышел final. То есть правильный вывод здесь не "Pyramid заброшен", а скорее: Pyramid поддерживается, но масштаб и устойчивость команды поддержки - фактор, который я обязательно учитывал бы при выборе его для нового проекта.
Иногда интереснее фреймворк, у которого после десяти лет эксплуатации всё ещё можно открыть внутреннюю архитектуру и обнаружить, что её части существуют не потому, что их постепенно прикручивали по мере появления "feature requests", а потому что за ними стоит последовательная модель того, как web-приложение вообще может быть устроено.
Я полез в Pyramid, чтобы сделать интеграцию с Jam. А в итоге нашёл довольно хороший повод ещё раз подумать о том, что именно я хочу получать от web-framework. И некоторые идеи Pyramid мы, кажется, действительно забыли слишком рано.
Источник: makridenko.com ↗