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 middlewareMiddleware 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 = appType 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
environwithout considering middleware. - Ignoring iterable cleanup.
Recommended practices
- 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.
Related guides
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.







