Python wsgiref: WSGI Applications

Published on: August 12, 2026
Reading time: 4 minutes
Web application code representing WSGI with Python wsgiref

The wsgiref package contains utilities and a reference implementation of WSGI, the traditional synchronous interface between Python web applications and web servers. It can build minimal applications, create test environments, manipulate response headers, validate protocol conformance, and run a simple HTTP server during development.

The documentation includes an important warning: wsgiref is not recommended for production and performs only basic security checks. Use it for learning, unit tests, middleware validation, and local prototypes. Deploy public applications behind a maintained WSGI server with appropriate concurrency, TLS, timeouts, limits, and monitoring.

The WSGI application contract

A WSGI application is a callable receiving environ and start_response. It calls start_response() with an HTTP status and response headers, then returns an iterable of bytes.

def app(environ, start_response):
    body = b"Hello, WSGI!\n"
    start_response(
        "200 OK",
        [
            ("Content-Type", "text/plain; charset=utf-8"),
            ("Content-Length", str(len(body))),
        ],
    )
    return [body]

Returning a Unicode string violates the specification. Body chunks must be bytes. The status string includes both code and reason phrase.

Run the reference server

from wsgiref.simple_server import make_server

with make_server("127.0.0.1", 8000, app) as server:
    print("http://127.0.0.1:8000")
    server.serve_forever()

The server is suitable for local examples and tests. It does not provide the robustness, performance, or hardening expected from a production deployment.

Understand the environ dictionary

environ contains CGI-style variables and WSGI keys. Common entries include REQUEST_METHOD, PATH_INFO, QUERY_STRING, CONTENT_TYPE, CONTENT_LENGTH, SERVER_NAME, SERVER_PORT, wsgi.input, wsgi.errors, and wsgi.url_scheme.

def app(environ, start_response):
    method = environ.get("REQUEST_METHOD", "GET")
    path = environ.get("PATH_INFO", "/")
    body = f"{method} {path}\n".encode("utf-8")
    start_response("200 OK", [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ])
    return [body]

Request-derived values are untrusted. Validate method, path, content length, headers, and body before using them.

Create a test environment

setup_testing_defaults() fills a dictionary with trivial WSGI values for tests. It must not be used by real servers because the data is artificial.

from wsgiref.util import setup_testing_defaults

environ = {}
setup_testing_defaults(environ)
environ["REQUEST_METHOD"] = "POST"
environ["PATH_INFO"] = "/items"

When the request has a body, supply a binary stream as wsgi.input.

Test without opening a port

from io import BytesIO
from wsgiref.util import setup_testing_defaults

captured = {}

def start_response(status, headers, exc_info=None):
    captured["status"] = status
    captured["headers"] = headers

environ = {}
setup_testing_defaults(environ)
environ["PATH_INFO"] = "/test"
environ["wsgi.input"] = BytesIO(b"")

body = b"".join(app(environ, start_response))
assert captured["status"] == "200 OK"

Direct testing is faster and more deterministic than starting a server for every case.

Validate WSGI conformance

wsgiref.validate.validator() wraps an application and checks many protocol requirements.

from wsgiref.validate import validator

validated_app = validator(app)

A detected violation generally raises AssertionError. Passing the validator does not prove complete compliance, but reported errors are strong evidence of a real problem.

Manipulate response headers

wsgiref.headers.Headers wraps a list of header pairs and treats names case-insensitively.

from wsgiref.headers import Headers

headers = Headers([])
headers["Content-Type"] = "text/plain; charset=utf-8"
headers.add_header(
    "Content-Disposition",
    "attachment",
    filename="report.txt",
)
response_headers = headers.items()

Unlike an ordinary dictionary, HTTP headers may repeat. Use get_all() for multi-valued fields such as Set-Cookie.

Hop-by-hop headers

is_hop_by_hop() identifies headers tied to one connection that must not be emitted as ordinary WSGI response headers.

from wsgiref.util import is_hop_by_hop

print(is_hop_by_hop("Connection"))

Servers and middleware should reject or handle these headers according to the specification.

Reconstruct request URLs

request_uri() reconstructs the full request URI, optionally including the query string. application_uri() returns the application base URI.

from wsgiref.util import request_uri, application_uri

url = request_uri(environ)
base = application_uri(environ)

Behind a reverse proxy, the environment may describe the internal hop. Trust forwarded headers only through an explicit trusted-proxy configuration.

Route with shift_path_info

shift_path_info() moves one segment from PATH_INFO to SCRIPT_NAME and mutates the dictionary.

from wsgiref.util import shift_path_info

def router(environ, start_response):
    child_environ = environ.copy()
    segment = shift_path_info(child_environ)
    if segment == "api":
        return api_app(child_environ, start_response)
    return not_found(child_environ, start_response)

Use a copy when other middleware needs the original path.

Read request bodies safely

wsgi.input is a byte stream. Read only the declared and permitted amount.

def read_body(environ, limit=1_000_000):
    raw = environ.get("CONTENT_LENGTH", "")
    try:
        length = int(raw) if raw else 0
    except ValueError:
        raise ValueError("invalid Content-Length")
    if length > limit:
        raise ValueError("request body too large")
    return environ["wsgi.input"].read(length)

Never call an unlimited read() on untrusted input.

Error responses

def response(status, text):
    body = text.encode("utf-8")
    return status, [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ], [body]

Do not expose tracebacks or internal environment values to clients. Log details to a protected error channel and return a generic message.

Build WSGI middleware

Middleware receives an application and returns another application.

def add_header(app):
    def middleware(environ, start_response):
        def begin(status, headers, exc_info=None):
            headers.append(("X-App", "demo"))
            return start_response(status, headers, exc_info)
        return app(environ, begin)
    return middleware

Middleware must preserve the protocol, correctly pass exceptions, and close returned iterables when required.

Close response iterables

An application may return an iterable with a close() method. Servers must call it. Manual tests should do the same.

result = app(environ, start_response)
try:
    body = b"".join(result)
finally:
    close = getattr(result, "close", None)
    if close:
        close()

Serve files with FileWrapper

FileWrapper turns a binary file object into a block iterator.

from wsgiref.util import FileWrapper

file = open("report.pdf", "rb")
return FileWrapper(file, blksize=64 * 1024)

Validate the path and set correct headers. Production servers often provide more efficient operating-system-level file transmission.

Static typing

Since Python 3.11, wsgiref.types includes protocols and aliases such as WSGIApplication, WSGIEnvironment, and StartResponse.

from wsgiref.types import WSGIApplication

application: WSGIApplication = app

Type checking can detect Unicode body chunks, incompatible signatures, and malformed header data before runtime.

WSGI is synchronous

WSGI models a synchronous call for each request. It remains relevant for traditional frameworks, but it does not natively model WebSockets or asynchronous concurrency like ASGI.

Do not return a coroutine from a WSGI application. Choose the server interface required by the framework and workload.

Why simple_server is not for production

The reference server is not a hardened Internet-facing platform. It lacks the operational features expected in production, including robust concurrency strategies, slow-client defenses, comprehensive limits, worker management, TLS operations, and observability.

A complete local example

from wsgiref.simple_server import make_server
from wsgiref.validate import validator


def app(environ, start_response):
    if environ.get("PATH_INFO") != "/":
        body = b"Not Found\n"
        start_response("404 Not Found", [
            ("Content-Type", "text/plain"),
            ("Content-Length", str(len(body))),
        ])
        return [body]

    body = b"Hello from WSGI\n"
    start_response("200 OK", [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ])
    return [body]

with make_server("127.0.0.1", 8000, validator(app)) as server:
    server.serve_forever()

Common mistakes

  • Returning strings instead of bytes.
  • Using the reference server in production.
  • Reading request bodies without a limit.
  • Trusting arbitrary proxy headers.
  • Exposing tracebacks to users.
  • Mutating environ without considering middleware.
  • Ignoring iterable cleanup.
  • Run validator() in tests.
  • Test applications directly without networking.
  • Set Content-Type and Content-Length.
  • Return bytes body chunks.
  • Enforce input limits.
  • Deploy with a production WSGI server.
  • Choose ASGI for native asynchronous workloads.

Continue with Python selectors, Python contextvars, Python platform, Python sysconfig, and Python pydoc.

See the official wsgiref documentation and PEP 3333.

Conclusion

wsgiref is excellent for learning WSGI, testing applications, and validating conformance. It makes the server-application contract explicit, but its simple server should never be exposed as a production service. Treat it as a reference and development tool.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Interactive terminal representing a custom REPL built with the Python code module
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python code: Build a Custom REPL

    Learn the Python code module to build custom REPLs, control namespaces, prompts, output, incomplete blocks, errors, and local exit behavior.

    Ler mais

    Tempo de leitura: 5 minutos
    12/08/2026
    Secure Internet protocol representing Unicode preparation with Python stringprep
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python stringprep: Prepare Unicode

    Learn Python stringprep to apply RFC 3454 tables, map Unicode, reject prohibited characters, and validate bidirectional protocol rules.

    Ler mais

    Tempo de leitura: 5 minutos
    12/08/2026
    Network connections representing non-blocking I/O with Python selectors
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python selectors: Non-Blocking I/O

    Learn Python selectors to monitor many sockets, read and write readiness, timeouts, partial messages, and non-blocking connections safely.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Network data flow representing asynchronous context with Python contextvars
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextvars: Async Context State

    Learn Python contextvars to store task-local state, prevent asyncio leaks, copy contexts, propagate metadata, and restore values with tokens.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Programming code representing operations as functions with Python operator
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python operator: Operations as Functions

    Learn Python operator to use operations as functions, sort fields, access items, call methods, and build clear functional pipelines.

    Ler mais

    Tempo de leitura: 4 minutos
    11/08/2026
    Three-dimensional alphabet representing Unicode normalization with Python unicodedata
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python unicodedata: Normalize Unicode

    Learn Python unicodedata to normalize Unicode, inspect names, categories, numeric values, combining marks, bidirectional classes, and width.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026