WSGI application in principle is simple enough, it could be any callable that accepts two arguments environ and start_response.

The environ contains the request data, client info and some server information, it is the responsablity of the server to accumulate the environ info correctly.

The start_response is a function that should be called with two arguments, the status and a list of headers as tuples, like this

start_response ( '200 ok' , [( 'Content-type' , 'application/json' )])

To return a valid response that can be rendered by the server to the client you have to always call the start_response first and then afterward yield a data string as a binary like this

yield b "<h1>My wsgi application response</h1>"

The simplest possible WSGI application is therefor thus

def my_wsgi ( environ , start_response ): start_response ( '200 ok' , [ 'Accept' , '*/*' ]) yield b 'Homepage'

Note that pep 333 does not recommend working directly with a wsgi application as a web programming API but instead use a high-level framework.

But for simple cases or as an exercise then you should be just fine.

Under the hood that's how all frameworks work.

To make a class callable like a function add the __call__ method to it, e.g.

class App ( object ): def __init ( self ): # bla bla pass def __call__ ( self ): return 'called as a function' instance = App () instance () # 'called as a function'

Here is an example of a usable WSGI application i wrote that registers urls somehow similar to flask route.

It only uses builtin libraries with a simple server and It's only 144 lines of code.

# wsgi.py import cgi from functools import wraps from wsgiref.simple_server import make_server try : import httplib except ImportError : import http.client as httplib # py3 class Request ( object ): """ Initiates a request object given the environ from the server """ def __init__ ( self , environ ): self . environ = environ self . headers = self . _parse_headers ( environ ) self . query = self . _parse_query ( environ ) self . data = self . _parse_data ( environ ) def _parse_query ( self , environ ): query = cgi . parse_qs ( environ [ 'QUERY_STRING' ]) return { k : v [ 0 ] for k , v in query . items ()} def _parse_headers ( self , environ ): length = environ . get ( 'CONTENT_LENGTH' , 0 ) headers = { 'CONTENT_LENGTH' : 0 if not length else int ( length )} wanted_headers = [ 'REQUEST_METHOD' , 'PATH_INFO' , 'REMOTE_ADDR' , 'REMOTE_HOST' , 'CONTENT_TYPE' ] for k , v in environ . items (): if k in wanted_headers or k . startswith ( 'HTTP' ): headers [ k ] = v return headers def _parse_data ( self , environ ): content_type = environ [ 'CONTENT_TYPE' ] . lower () data = {} if 'form' in content_type : env_data = cgi . FieldStorage ( environ [ 'wsgi.input' ], environ = environ ) for k in env_data . list : # filter out url queries if not isinstance ( k , cgi . MiniFieldStorage ): if k . filename : data [ k . name ] = k . file else : data [ k . name ] = k . value return data else : length = self . headers [ 'CONTENT_LENGTH' ] return environ [ 'wsgi.input' ] . read ( length ) class Response ( object ): """ Response object is responsable for initiating the make_response and returning the view data :params code, the status code :params data, the raw data rendered from the view """ def __init__ ( self , make_response , code = 200 , data = '' ): # view can return str or str and a dict of headers if isinstance ( data , tuple ): self . data = data [ 0 ] headers = data [ 1 ] else : self . data = data headers = {} if 'content-type' not in map ( lambda x : x . lower (), headers ): headers [ 'Content-Type' ] = 'text/html' self . headers = [( k , v ) for k , v in headers . items ()] self . code = code self . make_response = make_response def render ( self ): resp_code = '{} {}' . format ( self . code , httplib . responses [ self . code ]) if str ( self . code )[ 0 ] in [ '4' , '5' ]: self . make_response ( resp_code , self . headers ) yield resp_code . encode ( 'utf-8' ) try : data = bytes ( self . data ) except Exception : data = str ( self . data ) . encode ( 'utf-8' ) self . make_response ( resp_code , self . headers ) yield data class App ( object ): def __init__ ( self ): self . routes = {} def route ( self , url , methods = [ 'GET' ]): def decorate ( f ): @wraps ( f ) def wrapper ( * args , ** kwargs ): return f ( * args , ** kwargs ) self . routes [ url ] = { 'methods' : methods , 'func' : wrapper } return wrapper return decorate def path_dispatch ( self , request , make_response ): path = request . headers [ 'PATH_INFO' ] method = request . headers [ 'REQUEST_METHOD' ] view = self . routes . get ( path ) if not view : response = Response ( make_response , 404 ) elif method not in view [ 'methods' ]: response = Response ( make_response , 405 ) else : data = view [ 'func' ]( request ) response = Response ( make_response , data = data ) return response def dispatch_request ( self , environ , make_response ): request = Request ( environ ) response = self . path_dispatch ( request , make_response ) return response def __call__ ( self , environ , make_response ): """The actual wsgi app""" resp = self . dispatch_request ( environ , make_response ) return resp . render () def run ( self , host = '' , port = 8080 ): """ server """ httpd = make_server ( host , port , self ) print ( 'Serving on {host}:{port}' . format ( host = host , port = port )) httpd . serve_forever ()

Here are some possible ways to use it.

from wsgi import App app = App () @app.route ( '/' ) def home ( r ): # do something with headers print ( r . headers ) # do something with query print ( r . query ) return 'Welcome home' , { 'custom_headers' : 'header_be_here' } @app.route ( '/form' , methods = [ 'POST' ]) def form ( r ): # do something with form data and files print ( r . data ) return 'submitted successfully' if __name__ == '__main__' : app . run ()

Link for the code repository