python-peps/pep-0391/index.html

758 lines
56 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>PEP 391 Dictionary-Based Configuration For Logging | peps.python.org</title>
<link rel="shortcut icon" href="../_static/py.png">
<link rel="canonical" href="https://peps.python.org/pep-0391/">
<link rel="stylesheet" href="../_static/style.css" type="text/css">
<link rel="stylesheet" href="../_static/mq.css" type="text/css">
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" media="(prefers-color-scheme: light)" id="pyg-light">
<link rel="stylesheet" href="../_static/pygments_dark.css" type="text/css" media="(prefers-color-scheme: dark)" id="pyg-dark">
<link rel="alternate" type="application/rss+xml" title="Latest PEPs" href="https://peps.python.org/peps.rss">
<meta property="og:title" content='PEP 391 Dictionary-Based Configuration For Logging | peps.python.org'>
<meta property="og:description" content="This PEP describes a new way of configuring logging using a dictionary to hold configuration information.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://peps.python.org/pep-0391/">
<meta property="og:site_name" content="Python Enhancement Proposals (PEPs)">
<meta property="og:image" content="https://peps.python.org/_static/og-image.png">
<meta property="og:image:alt" content="Python PEPs">
<meta property="og:image:width" content="200">
<meta property="og:image:height" content="200">
<meta name="description" content="This PEP describes a new way of configuring logging using a dictionary to hold configuration information.">
<meta name="theme-color" content="#3776ab">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-sun-half" viewBox="0 0 24 24" pointer-events="all">
<title>Following system colour scheme</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"></circle>
<path d="M12 3v18m0-12l4.65-4.65M12 14.3l7.37-7.37M12 19.6l8.85-8.85"></path>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24" pointer-events="all">
<title>Selected dark colour scheme</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24" pointer-events="all">
<title>Selected light colour scheme</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
</svg>
<script>
document.documentElement.dataset.colour_scheme = localStorage.getItem("colour_scheme") || "auto"
</script>
<section id="pep-page-section">
<header>
<h1>Python Enhancement Proposals</h1>
<ul class="breadcrumbs">
<li><a href="https://www.python.org/" title="The Python Programming Language">Python</a> &raquo; </li>
<li><a href="../pep-0000/">PEP Index</a> &raquo; </li>
<li>PEP 391</li>
</ul>
<button id="colour-scheme-cycler" onClick="setColourScheme(nextColourScheme())">
<svg aria-hidden="true" class="colour-scheme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg aria-hidden="true" class="colour-scheme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg aria-hidden="true" class="colour-scheme-icon-when-light"><use href="#svg-sun"></use></svg>
<span class="visually-hidden">Toggle light / dark / auto colour theme</span>
</button>
</header>
<article>
<section id="pep-content">
<h1 class="page-title">PEP 391 Dictionary-Based Configuration For Logging</h1>
<dl class="rfc2822 field-list simple">
<dt class="field-odd">Author<span class="colon">:</span></dt>
<dd class="field-odd">Vinay Sajip &lt;vinay_sajip at red-dove.com&gt;</dd>
<dt class="field-even">Status<span class="colon">:</span></dt>
<dd class="field-even"><abbr title="Accepted and implementation complete, or no longer active">Final</abbr></dd>
<dt class="field-odd">Type<span class="colon">:</span></dt>
<dd class="field-odd"><abbr title="Normative PEP with a new feature for Python, implementation change for CPython or interoperability standard for the ecosystem">Standards Track</abbr></dd>
<dt class="field-even">Created<span class="colon">:</span></dt>
<dd class="field-even">15-Oct-2009</dd>
<dt class="field-odd">Python-Version<span class="colon">:</span></dt>
<dd class="field-odd">2.7, 3.2</dd>
<dt class="field-even">Post-History<span class="colon">:</span></dt>
<dd class="field-even"><p></p></dd>
</dl>
<hr class="docutils" />
<section id="contents">
<details><summary>Table of Contents</summary><ul class="simple">
<li><a class="reference internal" href="#abstract">Abstract</a></li>
<li><a class="reference internal" href="#rationale">Rationale</a></li>
<li><a class="reference internal" href="#specification">Specification</a><ul>
<li><a class="reference internal" href="#naming">Naming</a></li>
<li><a class="reference internal" href="#api">API</a></li>
<li><a class="reference internal" href="#dictionary-schema-overview">Dictionary Schema - Overview</a><ul>
<li><a class="reference internal" href="#object-connections">Object connections</a></li>
<li><a class="reference internal" href="#user-defined-objects">User-defined objects</a></li>
<li><a class="reference internal" href="#access-to-external-objects">Access to external objects</a></li>
<li><a class="reference internal" href="#access-to-internal-objects">Access to internal objects</a></li>
<li><a class="reference internal" href="#handler-ids">Handler Ids</a></li>
</ul>
</li>
<li><a class="reference internal" href="#dictionary-schema-detail">Dictionary Schema - Detail</a></li>
<li><a class="reference internal" href="#a-working-example">A Working Example</a></li>
</ul>
</li>
<li><a class="reference internal" href="#incremental-configuration">Incremental Configuration</a></li>
<li><a class="reference internal" href="#api-customization">API Customization</a></li>
<li><a class="reference internal" href="#change-to-socket-listener-implementation">Change to Socket Listener Implementation</a></li>
<li><a class="reference internal" href="#configuration-errors">Configuration Errors</a></li>
<li><a class="reference internal" href="#discussion-in-the-community">Discussion in the community</a></li>
<li><a class="reference internal" href="#reference-implementation">Reference implementation</a></li>
<li><a class="reference internal" href="#copyright">Copyright</a></li>
</ul>
</details></section>
<section id="abstract">
<h2><a class="toc-backref" href="#abstract" role="doc-backlink">Abstract</a></h2>
<p>This PEP describes a new way of configuring logging using a dictionary
to hold configuration information.</p>
</section>
<section id="rationale">
<h2><a class="toc-backref" href="#rationale" role="doc-backlink">Rationale</a></h2>
<p>The present means for configuring Pythons logging package is either
by using the logging API to configure logging programmatically, or
else by means of ConfigParser-based configuration files.</p>
<p>Programmatic configuration, while offering maximal control, fixes the
configuration in Python code. This does not facilitate changing it
easily at runtime, and, as a result, the ability to flexibly turn the
verbosity of logging up and down for different parts of a using
application is lost. This limits the usability of logging as an aid
to diagnosing problems - and sometimes, logging is the only diagnostic
aid available in production environments.</p>
<p>The ConfigParser-based configuration system is usable, but does not
allow its users to configure all aspects of the logging package. For
example, Filters cannot be configured using this system. Furthermore,
the ConfigParser format appears to engender dislike (sometimes strong
dislike) in some quarters. Though it was chosen because it was the
only configuration format supported in the Python standard at that
time, many people regard it (or perhaps just the particular schema
chosen for loggings configuration) as crufty or ugly, in some
cases apparently on purely aesthetic grounds.</p>
<p>Recent versions of Python include JSON support in the standard
library, and this is also usable as a configuration format. In other
environments, such as Google App Engine, YAML is used to configure
applications, and usually the configuration of logging would be
considered an integral part of the application configuration.
Although the standard library does not contain YAML support at
present, support for both JSON and YAML can be provided in a common
way because both of these serialization formats allow deserialization
to Python dictionaries.</p>
<p>By providing a way to configure logging by passing the configuration
in a dictionary, logging will be easier to configure not only for
users of JSON and/or YAML, but also for users of custom configuration
methods, by providing a common format in which to describe the desired
configuration.</p>
<p>Another drawback of the current ConfigParser-based configuration
system is that it does not support incremental configuration: a new
configuration completely replaces the existing configuration.
Although full flexibility for incremental configuration is difficult
to provide in a multi-threaded environment, the new configuration
mechanism will allow the provision of limited support for incremental
configuration.</p>
</section>
<section id="specification">
<h2><a class="toc-backref" href="#specification" role="doc-backlink">Specification</a></h2>
<p>The specification consists of two parts: the API and the format of the
dictionary used to convey configuration information (i.e. the schema
to which it must conform).</p>
<section id="naming">
<h3><a class="toc-backref" href="#naming" role="doc-backlink">Naming</a></h3>
<p>Historically, the logging package has not been <a class="pep reference internal" href="../pep-0008/" title="PEP 8 Style Guide for Python Code">PEP 8</a> conformant.
At some future time, this will be corrected by changing method and
function names in the package in order to conform with <a class="pep reference internal" href="../pep-0008/" title="PEP 8 Style Guide for Python Code">PEP 8</a>.
However, in the interests of uniformity, the proposed additions to the
API use a naming scheme which is consistent with the present scheme
used by logging.</p>
</section>
<section id="api">
<h3><a class="toc-backref" href="#api" role="doc-backlink">API</a></h3>
<p>The logging.config module will have the following addition:</p>
<ul class="simple">
<li>A function, called <code class="docutils literal notranslate"><span class="pre">dictConfig()</span></code>, which takes a single argument
- the dictionary holding the configuration. Exceptions will be
raised if there are errors while processing the dictionary.</li>
</ul>
<p>It will be possible to customize this API - see the section on <a class="reference internal" href="#api-customization">API
Customization</a>. <a class="reference internal" href="#incremental-configuration">Incremental configuration</a> is covered in its own
section.</p>
</section>
<section id="dictionary-schema-overview">
<h3><a class="toc-backref" href="#dictionary-schema-overview" role="doc-backlink">Dictionary Schema - Overview</a></h3>
<p>Before describing the schema in detail, it is worth saying a few words
about object connections, support for user-defined objects and access
to external and internal objects.</p>
<section id="object-connections">
<h4><a class="toc-backref" href="#object-connections" role="doc-backlink">Object connections</a></h4>
<p>The schema is intended to describe a set of logging objects - loggers,
handlers, formatters, filters - which are connected to each other in
an object graph. Thus, the schema needs to represent connections
between the objects. For example, say that, once configured, a
particular logger has attached to it a particular handler. For the
purposes of this discussion, we can say that the logger represents the
source, and the handler the destination, of a connection between the
two. Of course in the configured objects this is represented by the
logger holding a reference to the handler. In the configuration dict,
this is done by giving each destination object an id which identifies
it unambiguously, and then using the id in the source objects
configuration to indicate that a connection exists between the source
and the destination object with that id.</p>
<p>So, for example, consider the following YAML snippet:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">formatters</span><span class="p">:</span>
<span class="n">brief</span><span class="p">:</span>
<span class="c1"># configuration for formatter with id &#39;brief&#39; goes here</span>
<span class="n">precise</span><span class="p">:</span>
<span class="c1"># configuration for formatter with id &#39;precise&#39; goes here</span>
<span class="n">handlers</span><span class="p">:</span>
<span class="n">h1</span><span class="p">:</span> <span class="c1">#This is an id</span>
<span class="c1"># configuration of handler with id &#39;h1&#39; goes here</span>
<span class="n">formatter</span><span class="p">:</span> <span class="n">brief</span>
<span class="n">h2</span><span class="p">:</span> <span class="c1">#This is another id</span>
<span class="c1"># configuration of handler with id &#39;h2&#39; goes here</span>
<span class="n">formatter</span><span class="p">:</span> <span class="n">precise</span>
<span class="n">loggers</span><span class="p">:</span>
<span class="n">foo</span><span class="o">.</span><span class="n">bar</span><span class="o">.</span><span class="n">baz</span><span class="p">:</span>
<span class="c1"># other configuration for logger &#39;foo.bar.baz&#39;</span>
<span class="n">handlers</span><span class="p">:</span> <span class="p">[</span><span class="n">h1</span><span class="p">,</span> <span class="n">h2</span><span class="p">]</span>
</pre></div>
</div>
<p>(Note: YAML will be used in this document as it is a little more
readable than the equivalent Python source form for the dictionary.)</p>
<p>The ids for loggers are the logger names which would be used
programmatically to obtain a reference to those loggers, e.g.
<code class="docutils literal notranslate"><span class="pre">foo.bar.baz</span></code>. The ids for Formatters and Filters can be any string
value (such as <code class="docutils literal notranslate"><span class="pre">brief</span></code>, <code class="docutils literal notranslate"><span class="pre">precise</span></code> above) and they are transient,
in that they are only meaningful for processing the configuration
dictionary and used to determine connections between objects, and are
not persisted anywhere when the configuration call is complete.</p>
<p>Handler ids are treated specially, see the section on
<a class="reference internal" href="#handler-ids">Handler Ids</a>, below.</p>
<p>The above snippet indicates that logger named <code class="docutils literal notranslate"><span class="pre">foo.bar.baz</span></code> should
have two handlers attached to it, which are described by the handler
ids <code class="docutils literal notranslate"><span class="pre">h1</span></code> and <code class="docutils literal notranslate"><span class="pre">h2</span></code>. The formatter for <code class="docutils literal notranslate"><span class="pre">h1</span></code> is that described by id
<code class="docutils literal notranslate"><span class="pre">brief</span></code>, and the formatter for <code class="docutils literal notranslate"><span class="pre">h2</span></code> is that described by id
<code class="docutils literal notranslate"><span class="pre">precise</span></code>.</p>
</section>
<section id="user-defined-objects">
<h4><a class="toc-backref" href="#user-defined-objects" role="doc-backlink">User-defined objects</a></h4>
<p>The schema should support user-defined objects for handlers, filters
and formatters. (Loggers do not need to have different types for
different instances, so there is no support - in the configuration -
for user-defined logger classes.)</p>
<p>Objects to be configured will typically be described by dictionaries
which detail their configuration. In some places, the logging system
will be able to infer from the context how an object is to be
instantiated, but when a user-defined object is to be instantiated,
the system will not know how to do this. In order to provide complete
flexibility for user-defined object instantiation, the user will need
to provide a factory - a callable which is called with a
configuration dictionary and which returns the instantiated object.
This will be signalled by an absolute import path to the factory being
made available under the special key <code class="docutils literal notranslate"><span class="pre">'()'</span></code>. Heres a concrete
example:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">formatters</span><span class="p">:</span>
<span class="n">brief</span><span class="p">:</span>
<span class="nb">format</span><span class="p">:</span> <span class="s1">&#39;</span><span class="si">%(message)s</span><span class="s1">&#39;</span>
<span class="n">default</span><span class="p">:</span>
<span class="nb">format</span><span class="p">:</span> <span class="s1">&#39;</span><span class="si">%(asctime)s</span><span class="s1"> </span><span class="si">%(levelname)-8s</span><span class="s1"> </span><span class="si">%(name)-15s</span><span class="s1"> </span><span class="si">%(message)s</span><span class="s1">&#39;</span>
<span class="n">datefmt</span><span class="p">:</span> <span class="s1">&#39;%Y-%m-</span><span class="si">%d</span><span class="s1"> %H:%M:%S&#39;</span>
<span class="n">custom</span><span class="p">:</span>
<span class="p">():</span> <span class="n">my</span><span class="o">.</span><span class="n">package</span><span class="o">.</span><span class="n">customFormatterFactory</span>
<span class="n">bar</span><span class="p">:</span> <span class="n">baz</span>
<span class="n">spam</span><span class="p">:</span> <span class="mf">99.9</span>
<span class="n">answer</span><span class="p">:</span> <span class="mi">42</span>
</pre></div>
</div>
<p>The above YAML snippet defines three formatters. The first, with id
<code class="docutils literal notranslate"><span class="pre">brief</span></code>, is a standard <code class="docutils literal notranslate"><span class="pre">logging.Formatter</span></code> instance with the
specified format string. The second, with id <code class="docutils literal notranslate"><span class="pre">default</span></code>, has a
longer format and also defines the time format explicitly, and will
result in a <code class="docutils literal notranslate"><span class="pre">logging.Formatter</span></code> initialized with those two format
strings. Shown in Python source form, the <code class="docutils literal notranslate"><span class="pre">brief</span></code> and <code class="docutils literal notranslate"><span class="pre">default</span></code>
formatters have configuration sub-dictionaries:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">{</span>
<span class="s1">&#39;format&#39;</span> <span class="p">:</span> <span class="s1">&#39;</span><span class="si">%(message)s</span><span class="s1">&#39;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>and:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">{</span>
<span class="s1">&#39;format&#39;</span> <span class="p">:</span> <span class="s1">&#39;</span><span class="si">%(asctime)s</span><span class="s1"> </span><span class="si">%(levelname)-8s</span><span class="s1"> </span><span class="si">%(name)-15s</span><span class="s1"> </span><span class="si">%(message)s</span><span class="s1">&#39;</span><span class="p">,</span>
<span class="s1">&#39;datefmt&#39;</span> <span class="p">:</span> <span class="s1">&#39;%Y-%m-</span><span class="si">%d</span><span class="s1"> %H:%M:%S&#39;</span>
<span class="p">}</span>
</pre></div>
</div>
<p>respectively, and as these dictionaries do not contain the special key
<code class="docutils literal notranslate"><span class="pre">'()'</span></code>, the instantiation is inferred from the context: as a result,
standard <code class="docutils literal notranslate"><span class="pre">logging.Formatter</span></code> instances are created. The
configuration sub-dictionary for the third formatter, with id
<code class="docutils literal notranslate"><span class="pre">custom</span></code>, is:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">{</span>
<span class="s1">&#39;()&#39;</span> <span class="p">:</span> <span class="s1">&#39;my.package.customFormatterFactory&#39;</span><span class="p">,</span>
<span class="s1">&#39;bar&#39;</span> <span class="p">:</span> <span class="s1">&#39;baz&#39;</span><span class="p">,</span>
<span class="s1">&#39;spam&#39;</span> <span class="p">:</span> <span class="mf">99.9</span><span class="p">,</span>
<span class="s1">&#39;answer&#39;</span> <span class="p">:</span> <span class="mi">42</span>
<span class="p">}</span>
</pre></div>
</div>
<p>and this contains the special key <code class="docutils literal notranslate"><span class="pre">'()'</span></code>, which means that
user-defined instantiation is wanted. In this case, the specified
factory callable will be used. If it is an actual callable it will be
used directly - otherwise, if you specify a string (as in the example)
the actual callable will be located using normal import mechanisms.
The callable will be called with the <em>remaining</em> items in the
configuration sub-dictionary as keyword arguments. In the above
example, the formatter with id <code class="docutils literal notranslate"><span class="pre">custom</span></code> will be assumed to be
returned by the call:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">my</span><span class="o">.</span><span class="n">package</span><span class="o">.</span><span class="n">customFormatterFactory</span><span class="p">(</span><span class="n">bar</span><span class="o">=</span><span class="s1">&#39;baz&#39;</span><span class="p">,</span> <span class="n">spam</span><span class="o">=</span><span class="mf">99.9</span><span class="p">,</span> <span class="n">answer</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
</pre></div>
</div>
<p>The key <code class="docutils literal notranslate"><span class="pre">'()'</span></code> has been used as the special key because it is not a
valid keyword parameter name, and so will not clash with the names of
the keyword arguments used in the call. The <code class="docutils literal notranslate"><span class="pre">'()'</span></code> also serves as a
mnemonic that the corresponding value is a callable.</p>
</section>
<section id="access-to-external-objects">
<h4><a class="toc-backref" href="#access-to-external-objects" role="doc-backlink">Access to external objects</a></h4>
<p>There are times where a configuration will need to refer to objects
external to the configuration, for example <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code>. If the
configuration dict is constructed using Python code then this is
straightforward, but a problem arises when the configuration is
provided via a text file (e.g. JSON, YAML). In a text file, there is
no standard way to distinguish <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code> from the literal string
<code class="docutils literal notranslate"><span class="pre">'sys.stderr'</span></code>. To facilitate this distinction, the configuration
system will look for certain special prefixes in string values and
treat them specially. For example, if the literal string
<code class="docutils literal notranslate"><span class="pre">'ext://sys.stderr'</span></code> is provided as a value in the configuration,
then the <code class="docutils literal notranslate"><span class="pre">ext://</span></code> will be stripped off and the remainder of the
value processed using normal import mechanisms.</p>
<p>The handling of such prefixes will be done in a way analogous to
protocol handling: there will be a generic mechanism to look for
prefixes which match the regular expression
<code class="docutils literal notranslate"><span class="pre">^(?P&lt;prefix&gt;[a-z]+)://(?P&lt;suffix&gt;.*)$</span></code> whereby, if the <code class="docutils literal notranslate"><span class="pre">prefix</span></code>
is recognised, the <code class="docutils literal notranslate"><span class="pre">suffix</span></code> is processed in a prefix-dependent
manner and the result of the processing replaces the string value. If
the prefix is not recognised, then the string value will be left
as-is.</p>
<p>The implementation will provide for a set of standard prefixes such as
<code class="docutils literal notranslate"><span class="pre">ext://</span></code> but it will be possible to disable the mechanism completely
or provide additional or different prefixes for special handling.</p>
</section>
<section id="access-to-internal-objects">
<h4><a class="toc-backref" href="#access-to-internal-objects" role="doc-backlink">Access to internal objects</a></h4>
<p>As well as external objects, there is sometimes also a need to refer
to objects in the configuration. This will be done implicitly by the
configuration system for things that it knows about. For example, the
string value <code class="docutils literal notranslate"><span class="pre">'DEBUG'</span></code> for a <code class="docutils literal notranslate"><span class="pre">level</span></code> in a logger or handler will
automatically be converted to the value <code class="docutils literal notranslate"><span class="pre">logging.DEBUG</span></code>, and the
<code class="docutils literal notranslate"><span class="pre">handlers</span></code>, <code class="docutils literal notranslate"><span class="pre">filters</span></code> and <code class="docutils literal notranslate"><span class="pre">formatter</span></code> entries will take an
object id and resolve to the appropriate destination object.</p>
<p>However, a more generic mechanism needs to be provided for the case
of user-defined objects which are not known to logging. For example,
take the instance of <code class="docutils literal notranslate"><span class="pre">logging.handlers.MemoryHandler</span></code>, which takes
a <code class="docutils literal notranslate"><span class="pre">target</span></code> which is another handler to delegate to. Since the system
already knows about this class, then in the configuration, the given
<code class="docutils literal notranslate"><span class="pre">target</span></code> just needs to be the object id of the relevant target
handler, and the system will resolve to the handler from the id. If,
however, a user defines a <code class="docutils literal notranslate"><span class="pre">my.package.MyHandler</span></code> which has a
<code class="docutils literal notranslate"><span class="pre">alternate</span></code> handler, the configuration system would not know that
the <code class="docutils literal notranslate"><span class="pre">alternate</span></code> referred to a handler. To cater for this, a
generic resolution system will be provided which allows the user to
specify:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">handlers</span><span class="p">:</span>
<span class="n">file</span><span class="p">:</span>
<span class="c1"># configuration of file handler goes here</span>
<span class="n">custom</span><span class="p">:</span>
<span class="p">():</span> <span class="n">my</span><span class="o">.</span><span class="n">package</span><span class="o">.</span><span class="n">MyHandler</span>
<span class="n">alternate</span><span class="p">:</span> <span class="n">cfg</span><span class="p">:</span><span class="o">//</span><span class="n">handlers</span><span class="o">.</span><span class="n">file</span>
</pre></div>
</div>
<p>The literal string <code class="docutils literal notranslate"><span class="pre">'cfg://handlers.file'</span></code> will be resolved in an
analogous way to the strings with the <code class="docutils literal notranslate"><span class="pre">ext://</span></code> prefix, but looking
in the configuration itself rather than the import namespace. The
mechanism will allow access by dot or by index, in a similar way to
that provided by <code class="docutils literal notranslate"><span class="pre">str.format</span></code>. Thus, given the following snippet:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">handlers</span><span class="p">:</span>
<span class="n">email</span><span class="p">:</span>
<span class="n">class</span><span class="p">:</span> <span class="n">logging</span><span class="o">.</span><span class="n">handlers</span><span class="o">.</span><span class="n">SMTPHandler</span>
<span class="n">mailhost</span><span class="p">:</span> <span class="n">localhost</span>
<span class="n">fromaddr</span><span class="p">:</span> <span class="n">my_app</span><span class="nd">@domain</span><span class="o">.</span><span class="n">tld</span>
<span class="n">toaddrs</span><span class="p">:</span>
<span class="o">-</span> <span class="n">support_team</span><span class="nd">@domain</span><span class="o">.</span><span class="n">tld</span>
<span class="o">-</span> <span class="n">dev_team</span><span class="nd">@domain</span><span class="o">.</span><span class="n">tld</span>
<span class="n">subject</span><span class="p">:</span> <span class="n">Houston</span><span class="p">,</span> <span class="n">we</span> <span class="n">have</span> <span class="n">a</span> <span class="n">problem</span><span class="o">.</span>
</pre></div>
</div>
<p>in the configuration, the string <code class="docutils literal notranslate"><span class="pre">'cfg://handlers'</span></code> would resolve to
the dict with key <code class="docutils literal notranslate"><span class="pre">handlers</span></code>, the string <code class="docutils literal notranslate"><span class="pre">'cfg://handlers.email</span></code>
would resolve to the dict with key <code class="docutils literal notranslate"><span class="pre">email</span></code> in the <code class="docutils literal notranslate"><span class="pre">handlers</span></code> dict,
and so on. The string <code class="docutils literal notranslate"><span class="pre">'cfg://handlers.email.toaddrs[1]</span></code> would
resolve to <code class="docutils literal notranslate"><span class="pre">'dev_team.domain.tld'</span></code> and the string
<code class="docutils literal notranslate"><span class="pre">'cfg://handlers.email.toaddrs[0]'</span></code> would resolve to the value
<code class="docutils literal notranslate"><span class="pre">'support_team&#64;domain.tld'</span></code>. The <code class="docutils literal notranslate"><span class="pre">subject</span></code> value could be accessed
using either <code class="docutils literal notranslate"><span class="pre">'cfg://handlers.email.subject'</span></code> or, equivalently,
<code class="docutils literal notranslate"><span class="pre">'cfg://handlers.email[subject]'</span></code>. The latter form only needs to be
used if the key contains spaces or non-alphanumeric characters. If an
index value consists only of decimal digits, access will be attempted
using the corresponding integer value, falling back to the string
value if needed.</p>
<p>Given a string <code class="docutils literal notranslate"><span class="pre">cfg://handlers.myhandler.mykey.123</span></code>, this will
resolve to <code class="docutils literal notranslate"><span class="pre">config_dict['handlers']['myhandler']['mykey']['123']</span></code>.
If the string is specified as <code class="docutils literal notranslate"><span class="pre">cfg://handlers.myhandler.mykey[123]</span></code>,
the system will attempt to retrieve the value from
<code class="docutils literal notranslate"><span class="pre">config_dict['handlers']['myhandler']['mykey'][123]</span></code>, and fall back
to <code class="docutils literal notranslate"><span class="pre">config_dict['handlers']['myhandler']['mykey']['123']</span></code> if that
fails.</p>
</section>
<section id="handler-ids">
<h4><a class="toc-backref" href="#handler-ids" role="doc-backlink">Handler Ids</a></h4>
<p>Some specific logging configurations require the use of handler levels
to achieve the desired effect. However, unlike loggers which can
always be identified by their names, handlers have no persistent
handles whereby levels can be changed via an incremental configuration
call.</p>
<p>Therefore, this PEP proposes to add an optional <code class="docutils literal notranslate"><span class="pre">name</span></code> property to
handlers. If used, this will add an entry in a dictionary which maps
the name to the handler. (The entry will be removed when the handler
is closed.) When an incremental configuration call is made, handlers
will be looked up in this dictionary to set the handler level
according to the value in the configuration. See the section on
<a class="reference internal" href="#incremental-configuration">incremental configuration</a> for more details.</p>
<p>In theory, such a “persistent name” facility could also be provided
for Filters and Formatters. However, there is not a strong case to be
made for being able to configure these incrementally. On the basis
that practicality beats purity, only Handlers will be given this new
<code class="docutils literal notranslate"><span class="pre">name</span></code> property. The id of a handler in the configuration will
become its <code class="docutils literal notranslate"><span class="pre">name</span></code>.</p>
<p>The handler name lookup dictionary is for configuration use only and
will not become part of the public API for the package.</p>
</section>
</section>
<section id="dictionary-schema-detail">
<h3><a class="toc-backref" href="#dictionary-schema-detail" role="doc-backlink">Dictionary Schema - Detail</a></h3>
<p>The dictionary passed to <code class="docutils literal notranslate"><span class="pre">dictConfig()</span></code> must contain the following
keys:</p>
<ul class="simple">
<li><code class="docutils literal notranslate"><span class="pre">version</span></code> - to be set to an integer value representing the schema
version. The only valid value at present is 1, but having this key
allows the schema to evolve while still preserving backwards
compatibility.</li>
</ul>
<p>All other keys are optional, but if present they will be interpreted
as described below. In all cases below where a configuring dict is
mentioned, it will be checked for the special <code class="docutils literal notranslate"><span class="pre">'()'</span></code> key to see if a
custom instantiation is required. If so, the mechanism described
above is used to instantiate; otherwise, the context is used to
determine how to instantiate.</p>
<ul>
<li><code class="docutils literal notranslate"><span class="pre">formatters</span></code> - the corresponding value will be a dict in which each
key is a formatter id and each value is a dict describing how to
configure the corresponding Formatter instance.<p>The configuring dict is searched for keys <code class="docutils literal notranslate"><span class="pre">format</span></code> and <code class="docutils literal notranslate"><span class="pre">datefmt</span></code>
(with defaults of <code class="docutils literal notranslate"><span class="pre">None</span></code>) and these are used to construct a
<code class="docutils literal notranslate"><span class="pre">logging.Formatter</span></code> instance.</p>
</li>
<li><code class="docutils literal notranslate"><span class="pre">filters</span></code> - the corresponding value will be a dict in which each key
is a filter id and each value is a dict describing how to configure
the corresponding Filter instance.<p>The configuring dict is searched for key <code class="docutils literal notranslate"><span class="pre">name</span></code> (defaulting to the
empty string) and this is used to construct a <code class="docutils literal notranslate"><span class="pre">logging.Filter</span></code>
instance.</p>
</li>
<li><code class="docutils literal notranslate"><span class="pre">handlers</span></code> - the corresponding value will be a dict in which each
key is a handler id and each value is a dict describing how to
configure the corresponding Handler instance.<p>The configuring dict is searched for the following keys:</p>
<ul class="simple">
<li><code class="docutils literal notranslate"><span class="pre">class</span></code> (mandatory). This is the fully qualified name of the
handler class.</li>
<li><code class="docutils literal notranslate"><span class="pre">level</span></code> (optional). The level of the handler.</li>
<li><code class="docutils literal notranslate"><span class="pre">formatter</span></code> (optional). The id of the formatter for this
handler.</li>
<li><code class="docutils literal notranslate"><span class="pre">filters</span></code> (optional). A list of ids of the filters for this
handler.</li>
</ul>
<p>All <em>other</em> keys are passed through as keyword arguments to the
handlers constructor. For example, given the snippet:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>handlers:
console:
class : logging.StreamHandler
formatter: brief
level : INFO
filters: [allow_foo]
stream : ext://sys.stdout
file:
class : logging.handlers.RotatingFileHandler
formatter: precise
filename: logconfig.log
maxBytes: 1024
backupCount: 3
</pre></div>
</div>
<p>the handler with id <code class="docutils literal notranslate"><span class="pre">console</span></code> is instantiated as a
<code class="docutils literal notranslate"><span class="pre">logging.StreamHandler</span></code>, using <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code> as the underlying
stream. The handler with id <code class="docutils literal notranslate"><span class="pre">file</span></code> is instantiated as a
<code class="docutils literal notranslate"><span class="pre">logging.handlers.RotatingFileHandler</span></code> with the keyword arguments
<code class="docutils literal notranslate"><span class="pre">filename='logconfig.log',</span> <span class="pre">maxBytes=1024,</span> <span class="pre">backupCount=3</span></code>.</p>
</li>
<li><code class="docutils literal notranslate"><span class="pre">loggers</span></code> - the corresponding value will be a dict in which each key
is a logger name and each value is a dict describing how to
configure the corresponding Logger instance.<p>The configuring dict is searched for the following keys:</p>
<ul class="simple">
<li><code class="docutils literal notranslate"><span class="pre">level</span></code> (optional). The level of the logger.</li>
<li><code class="docutils literal notranslate"><span class="pre">propagate</span></code> (optional). The propagation setting of the logger.</li>
<li><code class="docutils literal notranslate"><span class="pre">filters</span></code> (optional). A list of ids of the filters for this
logger.</li>
<li><code class="docutils literal notranslate"><span class="pre">handlers</span></code> (optional). A list of ids of the handlers for this
logger.</li>
</ul>
<p>The specified loggers will be configured according to the level,
propagation, filters and handlers specified.</p>
</li>
<li><code class="docutils literal notranslate"><span class="pre">root</span></code> - this will be the configuration for the root logger.
Processing of the configuration will be as for any logger, except
that the <code class="docutils literal notranslate"><span class="pre">propagate</span></code> setting will not be applicable.</li>
<li><code class="docutils literal notranslate"><span class="pre">incremental</span></code> - whether the configuration is to be interpreted as
incremental to the existing configuration. This value defaults to
<code class="docutils literal notranslate"><span class="pre">False</span></code>, which means that the specified configuration replaces the
existing configuration with the same semantics as used by the
existing <code class="docutils literal notranslate"><span class="pre">fileConfig()</span></code> API.<p>If the specified value is <code class="docutils literal notranslate"><span class="pre">True</span></code>, the configuration is processed
as described in the section on <a class="reference internal" href="#incremental-configuration">Incremental Configuration</a>, below.</p>
</li>
<li><code class="docutils literal notranslate"><span class="pre">disable_existing_loggers</span></code> - whether any existing loggers are to be
disabled. This setting mirrors the parameter of the same name in
<code class="docutils literal notranslate"><span class="pre">fileConfig()</span></code>. If absent, this parameter defaults to <code class="docutils literal notranslate"><span class="pre">True</span></code>.
This value is ignored if <code class="docutils literal notranslate"><span class="pre">incremental</span></code> is <code class="docutils literal notranslate"><span class="pre">True</span></code>.</li>
</ul>
</section>
<section id="a-working-example">
<h3><a class="toc-backref" href="#a-working-example" role="doc-backlink">A Working Example</a></h3>
<p>The following is an actual working configuration in YAML format
(except that the email addresses are bogus):</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>formatters:
brief:
format: &#39;%(levelname)-8s: %(name)-15s: %(message)s&#39;
precise:
format: &#39;%(asctime)s %(name)-15s %(levelname)-8s %(message)s&#39;
filters:
allow_foo:
name: foo
handlers:
console:
class : logging.StreamHandler
formatter: brief
level : INFO
stream : ext://sys.stdout
filters: [allow_foo]
file:
class : logging.handlers.RotatingFileHandler
formatter: precise
filename: logconfig.log
maxBytes: 1024
backupCount: 3
debugfile:
class : logging.FileHandler
formatter: precise
filename: logconfig-detail.log
mode: a
email:
class: logging.handlers.SMTPHandler
mailhost: localhost
fromaddr: my_app@domain.tld
toaddrs:
- support_team@domain.tld
- dev_team@domain.tld
subject: Houston, we have a problem.
loggers:
foo:
level : ERROR
handlers: [debugfile]
spam:
level : CRITICAL
handlers: [debugfile]
propagate: no
bar.baz:
level: WARNING
root:
level : DEBUG
handlers : [console, file]
</pre></div>
</div>
</section>
</section>
<section id="incremental-configuration">
<h2><a class="toc-backref" href="#incremental-configuration" role="doc-backlink">Incremental Configuration</a></h2>
<p>It is difficult to provide complete flexibility for incremental
configuration. For example, because objects such as filters
and formatters are anonymous, once a configuration is set up, it is
not possible to refer to such anonymous objects when augmenting a
configuration.</p>
<p>Furthermore, there is not a compelling case for arbitrarily altering
the object graph of loggers, handlers, filters, formatters at
run-time, once a configuration is set up; the verbosity of loggers and
handlers can be controlled just by setting levels (and, in the case of
loggers, propagation flags). Changing the object graph arbitrarily in
a safe way is problematic in a multi-threaded environment; while not
impossible, the benefits are not worth the complexity it adds to the
implementation.</p>
<p>Thus, when the <code class="docutils literal notranslate"><span class="pre">incremental</span></code> key of a configuration dict is present
and is <code class="docutils literal notranslate"><span class="pre">True</span></code>, the system will ignore any <code class="docutils literal notranslate"><span class="pre">formatters</span></code> and
<code class="docutils literal notranslate"><span class="pre">filters</span></code> entries completely, and process only the <code class="docutils literal notranslate"><span class="pre">level</span></code>
settings in the <code class="docutils literal notranslate"><span class="pre">handlers</span></code> entries, and the <code class="docutils literal notranslate"><span class="pre">level</span></code> and
<code class="docutils literal notranslate"><span class="pre">propagate</span></code> settings in the <code class="docutils literal notranslate"><span class="pre">loggers</span></code> and <code class="docutils literal notranslate"><span class="pre">root</span></code> entries.</p>
<p>Its certainly possible to provide incremental configuration by other
means, for example making <code class="docutils literal notranslate"><span class="pre">dictConfig()</span></code> take an <code class="docutils literal notranslate"><span class="pre">incremental</span></code>
keyword argument which defaults to <code class="docutils literal notranslate"><span class="pre">False</span></code>. The reason for
suggesting that a value in the configuration dict be used is that it
allows for configurations to be sent over the wire as pickled dicts
to a socket listener. Thus, the logging verbosity of a long-running
application can be altered over time with no need to stop and
restart the application.</p>
<p>Note: Feedback on incremental configuration needs based on your
practical experience will be particularly welcome.</p>
</section>
<section id="api-customization">
<h2><a class="toc-backref" href="#api-customization" role="doc-backlink">API Customization</a></h2>
<p>The bare-bones <code class="docutils literal notranslate"><span class="pre">dictConfig()</span></code> API will not be sufficient for all
use cases. Provision for customization of the API will be made by
providing the following:</p>
<ul class="simple">
<li>A class, called <code class="docutils literal notranslate"><span class="pre">DictConfigurator</span></code>, whose constructor is passed
the dictionary used for configuration, and which has a
<code class="docutils literal notranslate"><span class="pre">configure()</span></code> method.</li>
<li>A callable, called <code class="docutils literal notranslate"><span class="pre">dictConfigClass</span></code>, which will (by default) be
set to <code class="docutils literal notranslate"><span class="pre">DictConfigurator</span></code>. This is provided so that if desired,
<code class="docutils literal notranslate"><span class="pre">DictConfigurator</span></code> can be replaced with a suitable user-defined
implementation.</li>
</ul>
<p>The <code class="docutils literal notranslate"><span class="pre">dictConfig()</span></code> function will call <code class="docutils literal notranslate"><span class="pre">dictConfigClass</span></code> passing
the specified dictionary, and then call the <code class="docutils literal notranslate"><span class="pre">configure()</span></code> method on
the returned object to actually put the configuration into effect:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">dictConfig</span><span class="p">(</span><span class="n">config</span><span class="p">):</span>
<span class="n">dictConfigClass</span><span class="p">(</span><span class="n">config</span><span class="p">)</span><span class="o">.</span><span class="n">configure</span><span class="p">()</span>
</pre></div>
</div>
<p>This should cater to all customization needs. For example, a subclass
of <code class="docutils literal notranslate"><span class="pre">DictConfigurator</span></code> could call <code class="docutils literal notranslate"><span class="pre">DictConfigurator.__init__()</span></code> in
its own <code class="docutils literal notranslate"><span class="pre">__init__()</span></code>, then set up custom prefixes which would be
usable in the subsequent <code class="docutils literal notranslate"><span class="pre">configure()</span> <span class="pre">call</span></code>. The <code class="docutils literal notranslate"><span class="pre">dictConfigClass</span></code>
would be bound to the subclass, and then <code class="docutils literal notranslate"><span class="pre">dictConfig()</span></code> could be
called exactly as in the default, uncustomized state.</p>
</section>
<section id="change-to-socket-listener-implementation">
<h2><a class="toc-backref" href="#change-to-socket-listener-implementation" role="doc-backlink">Change to Socket Listener Implementation</a></h2>
<p>The existing socket listener implementation will be modified as
follows: when a configuration message is received, an attempt will be
made to deserialize to a dictionary using the json module. If this
step fails, the message will be assumed to be in the fileConfig format
and processed as before. If deserialization is successful, then
<code class="docutils literal notranslate"><span class="pre">dictConfig()</span></code> will be called to process the resulting dictionary.</p>
</section>
<section id="configuration-errors">
<h2><a class="toc-backref" href="#configuration-errors" role="doc-backlink">Configuration Errors</a></h2>
<p>If an error is encountered during configuration, the system will raise
a <code class="docutils literal notranslate"><span class="pre">ValueError</span></code>, <code class="docutils literal notranslate"><span class="pre">TypeError</span></code>, <code class="docutils literal notranslate"><span class="pre">AttributeError</span></code> or <code class="docutils literal notranslate"><span class="pre">ImportError</span></code>
with a suitably descriptive message. The following is a (possibly
incomplete) list of conditions which will raise an error:</p>
<ul class="simple">
<li>A <code class="docutils literal notranslate"><span class="pre">level</span></code> which is not a string or which is a string not
corresponding to an actual logging level</li>
<li>A <code class="docutils literal notranslate"><span class="pre">propagate</span></code> value which is not a boolean</li>
<li>An id which does not have a corresponding destination</li>
<li>A non-existent handler id found during an incremental call</li>
<li>An invalid logger name</li>
<li>Inability to resolve to an internal or external object</li>
</ul>
</section>
<section id="discussion-in-the-community">
<h2><a class="toc-backref" href="#discussion-in-the-community" role="doc-backlink">Discussion in the community</a></h2>
<p>The PEP has been announced on python-dev and python-list. While there
hasnt been a huge amount of discussion, this is perhaps to be
expected for a niche topic.</p>
<p>Discussion threads on python-dev:</p>
<p><a class="reference external" href="https://mail.python.org/pipermail/python-dev/2009-October/092695.html">https://mail.python.org/pipermail/python-dev/2009-October/092695.html</a>
<a class="reference external" href="https://mail.python.org/pipermail/python-dev/2009-October/092782.html">https://mail.python.org/pipermail/python-dev/2009-October/092782.html</a>
<a class="reference external" href="https://mail.python.org/pipermail/python-dev/2009-October/093062.html">https://mail.python.org/pipermail/python-dev/2009-October/093062.html</a></p>
<p>And on python-list:</p>
<p><a class="reference external" href="https://mail.python.org/pipermail/python-list/2009-October/1223658.html">https://mail.python.org/pipermail/python-list/2009-October/1223658.html</a>
<a class="reference external" href="https://mail.python.org/pipermail/python-list/2009-October/1224228.html">https://mail.python.org/pipermail/python-list/2009-October/1224228.html</a></p>
<p>There have been some comments in favour of the proposal, no
objections to the proposal as a whole, and some questions and
objections about specific details. These are believed by the author
to have been addressed by making changes to the PEP.</p>
</section>
<section id="reference-implementation">
<h2><a class="toc-backref" href="#reference-implementation" role="doc-backlink">Reference implementation</a></h2>
<p>A reference implementation of the changes is available as a module
dictconfig.py with accompanying unit tests in test_dictconfig.py, at:</p>
<p><a class="reference external" href="http://bitbucket.org/vinay.sajip/dictconfig">http://bitbucket.org/vinay.sajip/dictconfig</a></p>
<p>This incorporates all features other than the socket listener change.</p>
</section>
<section id="copyright">
<h2><a class="toc-backref" href="#copyright" role="doc-backlink">Copyright</a></h2>
<p>This document has been placed in the public domain.</p>
</section>
</section>
<hr class="docutils" />
<p>Source: <a class="reference external" href="https://github.com/python/peps/blob/main/peps/pep-0391.rst">https://github.com/python/peps/blob/main/peps/pep-0391.rst</a></p>
<p>Last modified: <a class="reference external" href="https://github.com/python/peps/commits/main/peps/pep-0391.rst">2023-09-09 17:39:29 GMT</a></p>
</article>
<nav id="pep-sidebar">
<h2>Contents</h2>
<ul>
<li><a class="reference internal" href="#abstract">Abstract</a></li>
<li><a class="reference internal" href="#rationale">Rationale</a></li>
<li><a class="reference internal" href="#specification">Specification</a><ul>
<li><a class="reference internal" href="#naming">Naming</a></li>
<li><a class="reference internal" href="#api">API</a></li>
<li><a class="reference internal" href="#dictionary-schema-overview">Dictionary Schema - Overview</a><ul>
<li><a class="reference internal" href="#object-connections">Object connections</a></li>
<li><a class="reference internal" href="#user-defined-objects">User-defined objects</a></li>
<li><a class="reference internal" href="#access-to-external-objects">Access to external objects</a></li>
<li><a class="reference internal" href="#access-to-internal-objects">Access to internal objects</a></li>
<li><a class="reference internal" href="#handler-ids">Handler Ids</a></li>
</ul>
</li>
<li><a class="reference internal" href="#dictionary-schema-detail">Dictionary Schema - Detail</a></li>
<li><a class="reference internal" href="#a-working-example">A Working Example</a></li>
</ul>
</li>
<li><a class="reference internal" href="#incremental-configuration">Incremental Configuration</a></li>
<li><a class="reference internal" href="#api-customization">API Customization</a></li>
<li><a class="reference internal" href="#change-to-socket-listener-implementation">Change to Socket Listener Implementation</a></li>
<li><a class="reference internal" href="#configuration-errors">Configuration Errors</a></li>
<li><a class="reference internal" href="#discussion-in-the-community">Discussion in the community</a></li>
<li><a class="reference internal" href="#reference-implementation">Reference implementation</a></li>
<li><a class="reference internal" href="#copyright">Copyright</a></li>
</ul>
<br>
<a id="source" href="https://github.com/python/peps/blob/main/peps/pep-0391.rst">Page Source (GitHub)</a>
</nav>
</section>
<script src="../_static/colour_scheme.js"></script>
<script src="../_static/wrap_tables.js"></script>
<script src="../_static/sticky_banner.js"></script>
</body>
</html>