<?xml version="1.0" encoding="utf-8"?>
<feed version="0.3" xmlns="http://purl.org/atom/ns#">
<link rel="alternate" type="text/html" href="https://emergent.unpythonic.net/"/>

<title>Jeff Epler's blog</title>
<modified>2026-06-20T13:45:35Z</modified>
<tagline>Photos, electronics, cnc, and more</tagline>
<author><name>Jeff Epler</name><email>jepler@unpythonic.net</email></author>
<entry>
<title>Python multiline input: Using _pyrepl in your own code</title>
<issued>2026-06-20T13:45:35Z</issued>
<modified>2026-06-20T13:45:35Z</modified>
<id>https://emergent.unpythonic.net/01781963135</id>
<link rel="alternate" type="text/html" href="https://emergent.unpythonic.net/01781963135"/>
<content type="text/html" mode="escaped">

&lt;div style=&quot;float:right;clear:right&quot;&gt;&lt;!-- Screenshot_2026-06-20_09-41-11.png--&gt;&lt;div class=albumouter style=width:306px id=&gt;&lt;div class=albumimage style=&quot;width:199px;margin-left:53.5px;&quot;&gt;&lt;img src=&quot;https://media.unpythonic.net/emergent-files/01781963135/Screenshot_2026-06-20_09-41-11.png&quot;&gt;&lt;br&gt;&lt;span&gt;Colorized, multiline input in Python 3.14&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;p&gt;Starting with Python 3.13, a new internal module called &lt;code&gt;_pyrepl&lt;/code&gt; has been added to Python. It is used for the regular interactive prompt, and allows multiline editing. Python 3.14 adds syntax highlighting.&lt;/p&gt;
&lt;p&gt;I thought these features would be great to use in my own program, an rpn calculator modeled after the classic unix &lt;code&gt;dc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;_pyrepl&lt;/code&gt; is an internal module to Python, it's not intended for use by scripts. But, they can't stop us! Here's what I've learned...&lt;/p&gt;
&lt;h2&gt;Basics: Multiline editing&lt;/h2&gt;
&lt;p&gt;The dc language denotes strings with &lt;em&gt;balanced&lt;/em&gt; square brackets, so that &lt;code&gt;[x [a b] w]&lt;/code&gt; is a string. In terms of multiline editing, we want input to continue when there is an unbalanced open bracket character.&lt;/p&gt;
&lt;p&gt;Here's a simplistic implementation of a function wihch checks some input to find out whether it's balanced:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def count_brackets(s):
    return s.count(&amp;quot;[&amp;quot;) - s.count(&amp;quot;]&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, what we need is a true-or-false predicate. In Python zero is false and nonzero numbers are true so we could use &lt;code&gt;count_brackets&lt;/code&gt; directly, but I also wrote an actual predicate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def more_lines(s):
    return count_brackets(s) &amp;gt; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we can go ahead and do some multiline input:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from _pyrepl.readline import multiline_input  
while True:
    try:
        statement = multiline_input(more_lines, ps1, ps2)
    except EOFError:
        break
    print(repr(statement))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A session with this program might look like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pydc&amp;gt; 3 3 +
'3 3 +'
pydc&amp;gt; [a [
  ... b]
  ... c]
'[a [\nb]\nc]'
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Advanced: Syntax colorization&lt;/h2&gt;
&lt;p&gt;From Python 3.14, the Python repl colorizes Python code. We can repurpose this for highlighting our own syntax rather than Python syntax.&lt;/p&gt;
&lt;p&gt;In Python 3.14, it's necessary to monkey-patch the function &lt;code&gt;_pyrepl.reader.gen_colors&lt;/code&gt;. In Python 3.16 alphas, it is required to update the &lt;code&gt;gen_colors&lt;/code&gt; property of the &lt;code&gt;reader&lt;/code&gt; object. Happily, the same &lt;code&gt;gen_colors&lt;/code&gt; routine works in the same way in each version.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gen_colors&lt;/code&gt; generates a sequence of &lt;code&gt;ColorSpan&lt;/code&gt; objects. The &lt;code&gt;span&lt;/code&gt; member is an &lt;em&gt;inclusive&lt;/em&gt; range of characters, and the &lt;code&gt;tag&lt;/code&gt; must be one of several predefined &lt;em&gt;python&lt;/em&gt; syntax elements.&lt;/p&gt;
&lt;p&gt;Rather than show highlighting the dc language, I'll show a simple &lt;code&gt;gen_colors&lt;/code&gt; implementation, which colors digits and uppercase letters the way pyrepl shows numbers, lowercase letters the way pyrepl shows strings, and everything else in the terminal default color:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if hasattr(_pyrepl.utils, 'ColorSpan'):
    from _pyrepl.utils import ColorSpan, Span
    import _pyrepl.readline

    def gen_colors(s):
        for i, c in enumerate(s):
            if c in string.ascii_lowercase:
                yield ColorSpan(Span(i, i), &amp;quot;string&amp;quot;)
            elif c in string.digits or c in string.ascii_uppercase:
                yield ColorSpan(Span(i, i), &amp;quot;number&amp;quot;)
            else:
                yield ColorSpan(Span(i, i), &amp;quot;reset&amp;quot;)

    reader = _pyrepl.readline._wrapper.get_reader()
    if hasattr(reader, 'gen_colors'):
        reader.gen_colors = gen_colors
    else:
        import _pyrepl.reader
        _pyrepl.reader.gen_colors = gen_colors

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's worth noting that the builtin &lt;code&gt;gen_colors&lt;/code&gt; also takes care to emit fewer spans whenever possible, something I ignored for this example.&lt;/p&gt;
&lt;p&gt;Putting it all together, here's the full script:&lt;/p&gt;

&lt;p&gt;(Embedded not available - View &lt;a href=&quot;https://codeberg.org/jepler/junkdrawer/src/commit/main/gz0bnruv/mli.py&quot;&gt;gz0bnruv/mli.py&lt;/a&gt; on codeberg.org or &lt;a href=&quot;https://codeberg.org/jepler/junkdrawer/raw/commit/main/gz0bnruv/mli.py&quot;&gt;download raw&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;</content>
</entry>
</feed>
