pythonimmediate.simple module

Simple interface, suitable for users who may not be aware of \(\TeX\) subtleties, such as category codes.

This is only guaranteed to work properly in normal category code situations (in other words, \makeatletter or \ExplSyntaxOn are not officially supported). In all other cases, use the advanced API.

Nevertheless, there are still some parts that you must be aware of:

  • Any “output”, resulted from any command related to Python, can only be used to typeset text, it must not be used to pass “values” to other \(\TeX\) commands.

    This is already mentioned in the documentation of the \(\TeX\) file in \py command, just repeated here.

    For example:

    \py{1+1}  % legal, typeset 2
    \setcounter{abc}{\py{1+1}}  % illegal
    % the following is still illegal no many how many ``\expanded`` and such are used
    \py{ r'\\setcounter{abc}{' + str(1+1) + '}' }  % legal workaround

    Similar for commands defined with newcommand():

    def test():
        print_TeX("123", end="")

    Then the \(\TeX\) code:

    The result is \test.  % legal, typesets "123"
    \setcounter{abc}{\test}  % illegal
  • There’s the function fully_expand(), but some \(\TeX\) commands cannot be used inside this.

    Refer to the Note on unexpandable and fragile macros section for more details.

  • Regarding values being passed from \(\TeX\) to Python, you can do one of the following:

    • Either simply do \py{value = r'''#1'''}, or

    • \py{value = get_arg_str()}{#1}.

    You can easily see both forms are equivalent, except that in the first form, #1 cannot end with unbalanced \ or contain triple single quotes.

Start with reading newcommand() and execute(). Then read the rest of the functions in this module.

pythonimmediate.simple.define_char(char: str) Callable[[T2], T2][source]

Define a character to do some specific action.

Can be used as a decorator:

def multiplication_sign():


It’s not recommended to define commonly-used characters, for example if you define n then commands whose name contains n cannot be used anymore.

As another example, if you define -, then commands like \draw (1, 2) -- (3, 4); in TikZ cannot be used.

undefine_char() can be used to undo the effect.

pythonimmediate.simple.escape_str(s: str) str[source]

Given an arbitrary string, output some [LaTeX] code that when executed will typeset the original string.


>>> escape_str("123")
>>> escape_str(r"a%#_^~\?")
>>> escape_str("[")
>>> escape_str("  ")
'\\ \\ '
>>> escape_str("\n")

\n gets escaped to \\ so that multiple consecutive \n gives multiple line breaks.

< and > also needs to be escaped, but only in OT1 font encoding, see

>>> escape_str("<>")

Some Unicode characters may not be supported regardless.

Be careful with initial [ in text which may be interpreted as an optional argument in specific cases. For example

pythonimmediate.simple.execute(block: str, expecting_exit: bool = False) None[source]

Run a block of \(\TeX\)-code (might consist of multiple lines).

A simple example is execute('123') which simply typesets 123 (with a trailing newline, see the note in newcommand() documentation).

This is similar to print_TeX() but it’s executed immediately, and any error is immediately reported with Python traceback points to the caller.

On the other hand, because this is executed immediately, each part must be “self-contained”. With print_TeX() you can do the following:

print_TeX(r"\abc", end="")
print_TeX(r"def", end="")

and a single command \abcdef will be executed, but it does not work with this method.

As another consequence, all execute() are done before all print_TeX() in the same block. For example:


will typeset 1 2 3 4.


For advanced users: it’s implemented with \scantokens, so catcode-changing commands are allowed inside.

pythonimmediate.simple.execute_tokenized(line: str) None[source]


pythonimmediate.simple.f1(s: str, *, globals: Optional[dict] = None, locals: Optional[dict] = None, escape: Optional[Union[Tuple[str, ...], str]] = None) str[source]

Helper function to construct a string from Python expression parts, similar to f-strings, but allow using arbitrary delimiters.

This is useful for constructing \(\TeX\) code, because the { and } characters are frequently used in \(\TeX\).


>>> a = 1
>>> b = 2
>>> f1("a=`a`, b=`b`")
'a=1, b=2'
>>> f1("`")
Traceback (most recent call last):
ValueError: Unbalanced delimiters!

The escape/delimiter character can be escaped by doubling it:

>>> f1("a``b")
>>> f1("a ` '````' ` b")
'a `` b'

It’s possible to customize the delimiters:

>>> f1("a=!a!", escape="!")

It’s possible to use different delimiters for the opening and the closing:

>>> f1("a=⟨a⟩, b=⟨b⟩", escape="⟨⟩")
'a=1, b=2'
>>> f1("<", escape="<>")
Traceback (most recent call last):
ValueError: Unbalanced delimiters!

There’s no way to escape them, they must be balanced in the string:

>>> f1("a=⟨ '⟨'+str(a)+'⟩' ⟩", escape="⟨⟩")

The default value of escape is stored in f1.default_escape, it can be reassigned:

>>> f1.default_escape="!"
>>> f1("a=!a!")
>>> f1.default_escape="`"

It’s even possible to use multiple characters as the delimiter:

>>> f1("a=**a**, b=**b**", escape=["**"])
'a=1, b=2'
>>> f1("a=[[a]], b=[[b]]", escape=["[[", "]]"])
'a=1, b=2'

In the first case above, remember to put the delimiter in a list because otherwise it will be interpreted as an opening and a closing delimiter.

Currently, format specifiers such as :d are not supported.

pythonimmediate.simple.fully_expand(content: str) str[source]

Expand all macros in the given string.

Note on unexpandable and fragile macros

Not all \(\TeX\) commands/macros can be used inside this function. If you try to, they will either return the input unchanged, or error out.

In such cases, the problem is impossible to solve, so just look for workarounds.

An example is the \py command, which is already mentioned in the initial introduction to the simple module:

>> fully_expand(r"\py{1+1}")

Some other examples (assuming \usepackage{ifthen} is used):

>> execute(r'\ifthenelse{1=2}{equal}{not equal}')  # legal, typeset "not equal"
>> fully_expand(r'\ifthenelse{1=2}{equal}{not equal}')  # error out

>> execute(r'\uppercase{abc}')  # legal, typeset "ABC"
>> fully_expand(r'\uppercase{abc}')  # just returned verbatim
r'\uppercase {abc}'

There might be a few workarounds, but they’re all dependent on the exact macros involved.

For if-style commands, assign the desired result to a temporary variable:

>> execute(r'\ifthenelse{1=2}{\pyc{result = "equal"}}{\pyc{result = "not equal"}}')
>> result
"not equal"

For for-loop-style commands, you can do something similar (demonstrated here with tikz’s \foreach command):

>> result = []
>> execute(r'\foreach \x in {1, 2, ..., 10} {\pyc{result.append(var["x"])}}')
>> result
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']

For macros such as \uppercase{...}, there’s no simple workaround. Reimplement it yourself (such as with .upper() in Python).

pythonimmediate.simple.get_arg_estr() str[source]

Get a mandatory argument from the input stream, then process it as described in Note on argument expansion of estr-type functions.

Note on argument expansion of estr-type functions

These functions return a string, however they’re meant for strings other than \(\TeX\) code.

The argument is fully expanded, and any “escaped character” are passed as the character itself to Python.

The behavior is similar to that of the \py command argument processing, see also the \(\TeX\) package documentation.

Some examples: After the \(\TeX\) code \def\myvalue{123abc} is executed, then:

  • if the argument in \(\TeX\) code is {abc}, then the Python function will return "abc" (similar to as if get_arg_str() is used),

  • if the argument in \(\TeX\) code is {\%a\#b\\\~}, then the Python function will return r"%a#b\~",

  • if the argument in \(\TeX\) code is {\myvalue}, then the Python function will return "123abc".

pythonimmediate.simple.get_arg_str() str[source]

Get a mandatory argument from the input stream.

It’s slightly difficult to explain what this does, so here’s an example:

def foo():
    a = get_arg_str()
    b = get_arg_str()
    c = get_arg_str()

The above defines a \(\TeX\) function \foo, if it’s called in \(\TeX\) code as:


then the variable a will be the string "xx", b will be "yy", and c will be "zz".

Note on parameter tokenization


This function, as well as all the _str functions, return the argument that might be mangled in the following ways:

  • A space might be added after each command that consist of multiple characters

  • the ^^xx or ^^xxxx etc. notation is replaced by the character itself, or vice versa – literal tab character might get replaced with ^^I

  • multiple space characters may be collapsed into one

  • newline character may become space character

  • literally-typed \par in the source code may become double-newline character. (however you should never literally type \\par in the source code anyway he)

  • Even something like \\par in the source code may become double-newline character. (this is rare)

For example, \*\hell^^6F{  } may become the string r"\*\hello { }" in Python.

As such, verbatim-like commands in the argument are not supported. See get_verb_arg() for a workaround.

Nevertheless, if the argument consist of only “normal” \(\TeX\) commands, re-executing the string should do the same thing as the original code.

In case of doubt, print out the string and check it manually.


For advanced users:

This function corresponds to the m-type argument in xparse package.

It gets the argument, detokenize it, pass it through _replace_double_hash(), and return the result.

This is the simple API, as such it assumes normal category code values. Refer to get_next() for a more advanced API.

pythonimmediate.simple.get_env_body_verb_approximate(envname: Optional[str] = None) tuple[str, str, int][source]

This function is to be used inside newenvironment()’s registered function in order to get the environment body.

It can only be used once, and it only works if the argument is not already tokenized, i.e., it appears at “top-level”/does not appear inside any command or some environments such as align. (basically where a \verb|...| command can appear.)


a tuple (body, filename, line_number) where body is the body of the environment, filename is the filename of the TeX file, and line_number is the line number of the last line of the environment.


The returned code may have some tab characters converted to ^^I, trailing spaces stripped etc.

For advanced users: Basically it takes the following content until \end{⟨value of \@currenvir⟩}, as by the saveenv package.

pythonimmediate.simple.get_multiline_verb_arg() str[source]

Get a multi-line verbatim argument. Usage is identical to get_verb_arg(), except newline characters in the argument is supported.


in unusual category regime (such as that in \ExplSyntaxOn), it may return wrong result.

pythonimmediate.simple.get_next_char() str[source]

Return the character of the following token as with peek_next_char(), but also removes it from the input stream.

pythonimmediate.simple.get_optional_arg_estr() Optional[str][source]

Get an optional argument. See also Note on argument expansion of estr-type functions.

pythonimmediate.simple.get_optional_arg_str() Optional[str][source]

Get an optional argument. See also Note on parameter tokenization.


For advanced users: This function corresponds to the o-type argument in xparse package.

pythonimmediate.simple.get_verb_arg() str[source]

Get a verbatim argument.

Similar to \verb, defined \(\TeX\) commands that use this function can only be used at top level i.e. not inside any arguments.

This function behavior is identical to v-type argument in xparse package, you can use it like this:

\foo{xx%^\?}  % delimited with braces
\foo|xx%^\?|  % delimited with another symbol

See also

get_multiline_verb_arg() to support newline character in the argument.


Hard TAB character in the argument gives an error until the corresponding LaTeX3 bug is fixed, see

pythonimmediate.simple.is_balanced(content: str) bool[source]

Check if the given string is balanced, i.e. if all opening and closing braces match.

This is a bit tricky to implement, it’s recommended to use the library function.

For example:

>>> is_balanced("a{b}c")
>>> is_balanced("a{b}c}")
>>> is_balanced(r"\{")
pythonimmediate.simple.newcommand(name: str, f: Optional[Callable]) Callable[source]

Define a new \(\TeX\)-command.

The corresponding advanced API is Token.set_func().


def myfunction():
    print_TeX("Hello world!", end="")

The above is mostly equivalent to \newcommand{\myfunction}{Hello world!}, it defines a \(\TeX\) command \myfunction that prints Hello world!.

See also documentation of print_TeX() to understand the example above.

It can be used as either a decorator or a function:

def myfunction():
    print_TeX("Hello world!", end="")

newcommand("myfunction", myfunction)

An explicit command name can also be provided:

def myfunction():
    print_TeX("Hello world!", end="")

The above defines a \(\TeX\) command \hello that prints Hello world!.

If name is not provided, it’s automatically deduced from the Python function name.

The above is not by itself very useful. Read the documentation of the following functions sequentially for a guide on how to define commands that take arguments:

Then see also (as natural extensions of the above):

Note on trailing newline


Regarding the end="" part in the example above, it’s used to prevent a spurious space, otherwise if you run the \(\TeX\) code:

123\myfunction 456

it will be “equivalent” to:

123Hello world!

and the newline inserts a space.

Internally, the feature is implemented by appending % to the last line, as most of the time the code:

123Hello world!%

will be equivalent to:

123Hello world!456

but in unusual category code cases or cases of malformed \(\TeX\) code (such as a trailing backslash at the end of the line), it may not be equivalent. Use the advanced API, or always include a final newline and manually add the comment character, if these cases happen.

pythonimmediate.simple.newenvironment(name: str, f: Optional[Callable]) Callable[source]

Define a new \(\TeX\) normal environment.

Note that the environment will normally not have access to the body of the environment, see newenvironment_verb() for some alternatives.

  • name – the name of the environment, e.g. "myenv" or "myenv*".

  • f – a function that should execute the code for the begin part of the environment, yield, then execute the code for the end part of the environment.

  • engine – the engine to use.

Usage example:

def myenv():
    x=random.randint(1, 100)
    print_TeX(f"begin {x}")
    print_TeX(f"end {x}")

then the following \(\TeX\) code:

Hello world!

might typeset the following content:

begin 42
begin 24
Hello world!
end 24
end 42

Functions such as get_arg_str() etc. can also be used in the first part of the function to take arguments.

If the name is omitted, the function name is used as the environment name.

It can be used either as a decorator or a function, see newcommand() for details.

pythonimmediate.simple.newenvironment_verb(name: str, f: Optional[Callable]) Callable[source]

Define a new \(\TeX\) environment that reads its body verbatim.

Note that space characters at the end of each line are removed.

The environment must not take any argument. For example the following built-in tabular environment takes some argument, so cannot be implemented with this function:


It can be used either as a decorator or a function, see newenvironment() for details.

Some usage example:

def myenv(body: str):
    execute(body.replace("a", "b"))

If later the following \(\TeX\) code is executed:


then the value of the variable body will be "aaa\n", and the following content will be typeset:



For advanced users: unlike a typical environment, the code will not be executed in a new \(\TeX\) group.

pythonimmediate.simple.parse_keyval(content: str, allow_duplicate: bool = False) dict[str, Optional[str]][source]

Parse a key-value string into a dictionary.

>>> parse_keyval("a=b,c=d")
{'a': 'b', 'c': 'd'}
>>> parse_keyval("a,c=d")
{'a': None, 'c': 'd'}
>>> parse_keyval("a = b , c = d")
{'a': 'b', 'c': 'd'}
>>> parse_keyval("a ={ b }, c ={ d}")
{'a': ' b ', 'c': ' d'}
>>> parse_keyval("a={b,c}, c=d")
{'a': 'b,c', 'c': 'd'}
>>> parse_keyval("a=b,a=c")
Traceback (most recent call last):
ValueError: Duplicate key: 'a'
>>> parse_keyval("a=b,a=c", allow_duplicate=True)
{'a': 'c'}
pythonimmediate.simple.parse_keyval_items(content: str) list[tuple[str, Optional[str]]][source]
>>> parse_keyval_items("a=b,c=d")
[('a', 'b'), ('c', 'd')]
>>> parse_keyval_items("a,c=d")
[('a', None), ('c', 'd')]
>>> parse_keyval_items("a = b , c = d")
[('a', 'b'), ('c', 'd')]
>>> parse_keyval_items("a ={ b }, c ={ d}")
[('a', ' b '), ('c', ' d')]
>>> parse_keyval_items("a={b,c}, c=d")
[('a', 'b,c'), ('c', 'd')]
>>> parse_keyval_items("{a=b},c=d")
[('{a=b}', None), ('c', 'd')]
pythonimmediate.simple.peek_next_char() str[source]

Get the character of the following token, or empty string if it’s not a character.

This function can be used as follows to simulate e-type argument specifiers of xparse:

if peek_next_char()=="^":
    result = get_arg_str()
    # the following content in the input stream is ``^{...}`` and we stored the content inside to ``result``
    pass  # there's no ``^...`` following in the input stream

It can also simulate s-type argument specifier (optional star):

if peek_next_char()=="*":
    # there's a star
    pass  # there's no star

It can even simulate o-type argument specifier (optional argument delimited by [...]):

if peek_next_char()=="[":
    get_next_char()  # skip the `[`
    while True:
        if peek_next_char():
            if c=="]": break
            # following in the input stream must be a control sequence, such as `\relax`
    # now result contains the content inside the `[...]`
    pass  # there's no optional argument

Note that the above does not take into account the balance of braces or brackets, so:

  • If the following content in the input is [ab{cd]ef}gh] then the result will be ab{cd.

  • If the following content in the input is [ab[cd]ef] then the result will be ab[cd.


For advanced users:

This function uses peek_next_meaning() under the hood to get the meaning of the following token. See that function documentation for a warning on undefined behavior.

Will also return nonempty if the next token is an implicit character token. This case is not supported and might give random error.

pythonimmediate.simple.print_TeX(*args: Any, **kwargs: Any) None[source]

Unlike other packages, the normal print() function prints to the console.

This function can be used to print \(\TeX\) code to be executed. For example:

print_TeX("Hello world!")

It can also be used inside a custom-defined command, see the documentation of newcommand() for an example.

The signature is identical to that of print(). Passing explicit file= argument is not allowed.

Remark: normally the print() function prints out a newline with each invocation. A newline will appear as a spurious space in the output Use print_TeX(end="") to prevent this, see Note on trailing newline.

pythonimmediate.simple.put_next(arg: str) None[source]

Put some content forward in the input stream.



The content, must be a single line.

Note that there must not be any verbatim-like commands in the argument, so for example put_next(r"\verb|a|") is not allowed.

If there might be verbatim-like arguments, the problem is (almost) unsolvable. Refer to print_TeX() or execute() for workarounds, or use the advanced interface such as pythonimmediate.BalancedTokenList.put_next().

For example:

>>> put_next("{abc}")
>>> put_next("{def}")
>>> get_arg_str()
>>> get_arg_str()


For advanced users: the argument is tokenized in the current category regime.

pythonimmediate.simple.put_next_tokenized(line: str) None[source]


pythonimmediate.simple.renewcommand(name: str, f: Optional[Callable]) Callable[source]

Redefine a \(\TeX\)-command. Usage is similar to newcommand().

pythonimmediate.simple.split_balanced(content: str, /, sep: str, maxsplit: int = -1, do_strip_braces_in_result: bool = True) List[str][source]

Split the given string at the given substring, but only if the parts are balanced.

This is a bit tricky to implement, it’s recommended to use the library function.

If either content or sep is unbalanced, the function will raise ValueError.

It is recommended to set do_strip_braces_in_result to True (the default), otherwise the user will not have any way to “quote” the separator in each entry.

For example:

>>> split_balanced("a{b,c},c{d}", ",")
['a{b,c}', 'c{d}']
>>> split_balanced("a{b,c},{d,d},e", ",", do_strip_braces_in_result=False)
['a{b,c}', '{d,d}', 'e']
>>> split_balanced("a{b,c},{d,d},e", ",")
['a{b,c}', 'd,d', 'e']
>>> split_balanced(" a = b = c ", "=", maxsplit=1)
[' a ', ' b = c ']
>>> split_balanced(r"\{,\}", ",")
['\\{', '\\}']
>>> split_balanced("{", "{")
Traceback (most recent call last):
ValueError: Content is not balanced!
pythonimmediate.simple.strip_optional_braces(content: str) str[source]

Strip the optional braces from the given string, if the whole string is wrapped in braces.

For example:

>>> strip_optional_braces("{a}")
>>> strip_optional_braces("a")
>>> strip_optional_braces("{a},{b}")
pythonimmediate.simple.undefine_char(char: str) None[source]

The opposite of define_char().

pythonimmediate.simple.var = VarManager()

Get and set value of “variables” in \(\TeX\).

Can be used like this:

var["myvar"]="myvalue"  # "equivalent" to \def\myvar{myvalue}
var["myvar"]=r"\textbf{123}"  # "equivalent" to \def\myvar{\textbf{123}}
var["myvar"]=r"\def\test#1{#1}"  # "equivalent" to \tl_set:Nn \myvar {\def\test#1{#1}}  (note that `#` doesn't need to be doubled here unlike in `\def`)
var()["myvar"]="myvalue"  # pass explicit engine

# after ``\def\myvar{myvalue}`` (or ``\edef``, etc.) on TeX you can do:
print(var["myvar"])  # get the value of the variable, return a string

It’s currently undefined behavior if the passed-in value is not a “variable”. (in some future version additional checking might be performed in debug run)

Notes in Note on parameter tokenization apply – in other words, after you set a value it may become slightly different when read back.