Infix to Postfix in Python

A complete infix to postfix converter in Python — the conversion itself, a step-by-step trace for showing your working, and an evaluator that computes the result. Every listing here was run before it was published.

The complete Python program

Save this as infix_to_postfix.py. It needs Python 3.9 or newer (for the list[str] annotations) and no third-party packages. It handles multi-character operands, decimals, all six operators and nested brackets.

infix_to_postfix.py
"""Convert an infix expression to postfix (Reverse Polish Notation) using a stack."""

PRECEDENCE = {"^": 3, "*": 2, "/": 2, "%": 2, "+": 1, "-": 1}
RIGHT_ASSOCIATIVE = {"^"}


def tokenize(expression: str) -> list[str]:
    """Split an infix string into operands, operators and brackets.

    Multi-character operands are kept whole, so "rate * 12" gives
    ["rate", "*", "12"] rather than one token per character.
    """
    tokens: list[str] = []
    i = 0
    while i < len(expression):
        char = expression[i]

        if char.isspace():
            i += 1
        elif char.isalnum() or char in "_.":
            start = i
            while i < len(expression) and (expression[i].isalnum() or expression[i] in "_."):
                i += 1
            tokens.append(expression[start:i])
        elif char in PRECEDENCE or char in "()":
            tokens.append(char)
            i += 1
        else:
            raise ValueError(f"invalid character: {char!r}")

    return tokens


def infix_to_postfix(expression: str) -> str:
    """Return the postfix form of an infix expression."""
    output: list[str] = []
    stack: list[str] = []

    for token in tokenize(expression):
        # Order matters: test brackets and operators first, so anything
        # left over is an operand.
        if token == "(":
            stack.append(token)

        elif token == ")":
            while stack and stack[-1] != "(":
                output.append(stack.pop())
            if not stack:
                raise ValueError("unbalanced brackets: an extra ')'")
            stack.pop()                      # discard the matching "("

        elif token in PRECEDENCE:
            while (
                stack
                and stack[-1] != "("
                and (
                    PRECEDENCE[stack[-1]] > PRECEDENCE[token]
                    or (
                        PRECEDENCE[stack[-1]] == PRECEDENCE[token]
                        and token not in RIGHT_ASSOCIATIVE
                    )
                )
            ):
                output.append(stack.pop())
            stack.append(token)

        else:
            output.append(token)

    while stack:
        top = stack.pop()
        if top == "(":
            raise ValueError("unbalanced brackets: a missing ')'")
        output.append(top)

    return " ".join(output)


if __name__ == "__main__":
    expression = input("Enter an infix expression: ")
    try:
        print("Postfix expression:", infix_to_postfix(expression))
    except ValueError as error:
        print("Error:", error)

Run it

python3 infix_to_postfix.py

On Windows the command is usually python infix_to_postfix.py. You can also import it rather than running it interactively:

>>> from infix_to_postfix import infix_to_postfix
>>> infix_to_postfix("A + B * C")
'A B C * +'

Sample output

Enter an infix expression: A+B*C
Postfix expression: A B C * +

Enter an infix expression: (A+B)*C-D
Postfix expression: A B + C * D -

Enter an infix expression: rate * 12 + 250
Postfix expression: rate 12 * 250 +

Enter an infix expression: (A+B
Error: unbalanced brackets: a missing ')'

Those are the real outputs of the listing above. Run the same expressions through the converter on the homepage to compare against the full stack trace.

How it works

The program is two functions. tokenize() turns the input string into a list of operands, operators and brackets. infix_to_postfix() runs Dijkstra's shunting-yard algorithm over that list using an ordinary Python list as the stack.

What each part of the program does
PartPurpose
PRECEDENCEMaps each operator to a number — and in PRECEDENCE doubles as the "is this an operator?" test
RIGHT_ASSOCIATIVEA set holding only ^
tokenize()Groups runs of letters, digits, _ and . into whole operands
stack.append()Push
stack.pop()Pop
stack[-1]Peek — reads the top without removing it
if not stackThe empty test; an empty list is falsy
Why the branches are in that order

The loop tests "(", then ")", then token in PRECEDENCE, and treats everything left over as an operand. Testing for the operand first is tempting but fragile — a check like token not in "()" uses substring matching, so it behaves unexpectedly for multi-character tokens. Leaving the operand as the else branch avoids the question entirely.

The two rules that decide correctness

Everything about whether this program is right or wrong lives in one while condition.

the condition that matters
while (
    stack
    and stack[-1] != "("
    and (
        PRECEDENCE[stack[-1]] > PRECEDENCE[token]
        or (
            PRECEDENCE[stack[-1]] == PRECEDENCE[token]
            and token not in RIGHT_ASSOCIATIVE
        )
    )
):
    output.append(stack.pop())
stack.append(token)
  • The == branch. Equal precedence must also pop, for left-associative operators. Delete it and A - B - C becomes A B C - -, which means A - (B - C).
  • The not in RIGHT_ASSOCIATIVE guard. Without it, a ^ b ^ c becomes a b ^ c ^ instead of a b c ^ ^.
  • The stack[-1] != "(" guard. An opening bracket is a floor. Popping must never run past it.

This is the same condition as in the C version and in the pseudocode.

Printing a step-by-step trace

If your assignment asks you to show the stack at each step, this version prints the table for you. It imports the pieces from the program above rather than repeating them.

trace.py
"""Print the stack and the output after every token, the way an exam answer wants it."""

from infix_to_postfix import PRECEDENCE, RIGHT_ASSOCIATIVE, tokenize


def infix_to_postfix_traced(expression: str) -> list[str]:
    """Convert to postfix, printing one table row per token."""
    output: list[str] = []
    stack: list[str] = []

    print(f"{'TOKEN':<8}{'ACTION':<34}{'STACK':<16}OUTPUT")
    print("-" * 78)

    def row(token: str, action: str) -> None:
        print(f"{token:<8}{action:<34}{' '.join(stack) or '-':<16}{' '.join(output) or '-'}")

    for token in tokenize(expression):
        if token == "(":
            stack.append(token)
            row(token, "push as a marker")

        elif token == ")":
            popped = []
            while stack and stack[-1] != "(":
                popped.append(stack.pop())
            output.extend(popped)
            stack.pop()
            row(token, f"pop {', '.join(popped)}, drop the (" if popped else "drop the (")

        elif token in PRECEDENCE:
            popped = []
            while (
                stack
                and stack[-1] != "("
                and (
                    PRECEDENCE[stack[-1]] > PRECEDENCE[token]
                    or (
                        PRECEDENCE[stack[-1]] == PRECEDENCE[token]
                        and token not in RIGHT_ASSOCIATIVE
                    )
                )
            ):
                popped.append(stack.pop())
            output.extend(popped)
            stack.append(token)
            row(token, f"pop {', '.join(popped)}, push {token}" if popped else f"push {token}")

        else:
            output.append(token)
            row(token, "operand, straight to output")

    tail = stack[::-1]
    output.extend(tail)
    stack.clear()
    row("end", f"pop {', '.join(tail)}" if tail else "nothing left")

    return output


if __name__ == "__main__":
    result = infix_to_postfix_traced("A + B * C - (D / E)")
    print("\nPostfix:", " ".join(result))

Running it prints:

TOKEN   ACTION                            STACK           OUTPUT
------------------------------------------------------------------------------
A       operand, straight to output       -               A
+       push +                            +               A
B       operand, straight to output       +               A B
*       push *                            + *             A B
C       operand, straight to output       + *             A B C
-       pop *, +, push -                  -               A B C * +
(       push as a marker                  - (             A B C * +
D       operand, straight to output       - (             A B C * + D
/       push /                            - ( /           A B C * + D
E       operand, straight to output       - ( /           A B C * + D E
)       pop /, drop the (                 -               A B C * + D E /
end     pop -                             -               A B C * + D E / -

Postfix: A B C * + D E / -

Evaluating the postfix result

Once an expression is in postfix, evaluating it takes a second stack and one pass: push operands, and on each operator pop two values, apply it, push the result.

evaluate.py
"""Evaluate a postfix expression with a second stack."""

import operator

from infix_to_postfix import infix_to_postfix

OPERATIONS = {
    "+": operator.add,
    "-": operator.sub,
    "*": operator.mul,
    "/": operator.truediv,
    "%": operator.mod,
    "^": operator.pow,
}


def evaluate_postfix(postfix: str) -> float:
    """Push operands; on each operator pop two values and push the result."""
    stack: list[float] = []

    for token in postfix.split():
        if token in OPERATIONS:
            if len(stack) < 2:
                raise ValueError(f"not enough operands for '{token}'")
            right = stack.pop()
            left = stack.pop()
            stack.append(OPERATIONS[token](left, right))
        else:
            stack.append(float(token))

    if len(stack) != 1:
        raise ValueError("malformed postfix expression")
    return stack[0]


if __name__ == "__main__":
    for infix in ["2 + 3 * 4", "(2 + 3) * 4", "3 + 4 * 2 / (1 - 5) ^ 2", "2 ^ 3 ^ 2", "100 / 5 / 2"]:
        postfix = infix_to_postfix(infix)
        print(f"{infix:<26} -> {postfix:<24} = {evaluate_postfix(postfix):g}")
2 + 3 * 4                  -> 2 3 4 * +                = 14
(2 + 3) * 4                -> 2 3 + 4 *                = 20
3 + 4 * 2 / (1 - 5) ^ 2    -> 3 4 2 * 1 5 - 2 ^ / +    = 3.5
2 ^ 3 ^ 2                  -> 2 3 2 ^ ^                = 512
100 / 5 / 2                -> 100 5 / 2 /              = 10
Note the operand order

right = stack.pop() comes before left = stack.pop(). The second value popped is the left operand, because it was pushed first. Swap those two lines and addition still works but subtraction, division and exponentiation all silently invert — 2 3 2 ^ ^ would give 81 instead of 512.

Notes for Python specifically

  • A list is already a stack. append() and pop() are both O(1) at the end. There is no reason to write a Stack class, and collections.deque buys you nothing here.
  • Do not use pop(0). That pops the front, which is a queue, not a stack, and it is O(n).
  • stack[-1] is peek. It raises IndexError on an empty list, which is why every use is guarded by stack and …; Python's and short-circuits, so the index is never evaluated when the stack is empty.
  • Do not use eval(). It is the usual shortcut for "evaluate this expression" and it executes arbitrary code. The postfix evaluator above is a dozen lines and cannot run anything.
  • str.isalnum() is Unicode-aware. It returns True for letters beyond ASCII, so café is accepted as one operand. That is usually what you want; if you need ASCII only, test against string.ascii_letters instead.

Errors you are likely to hit

Common problems in an infix to postfix Python program
SymptomCauseFix
IndexError: pop from empty listPeeking or popping without checking the stack firstGuard with while stack and …
KeyError: '('Looking up a bracket in PRECEDENCEKeep the stack[-1] != "(" test before the lookup
A-B-C gives A B C - -Equal precedence pushes instead of poppingAdd the == branch
a^b^c gives a b ^ c ^^ treated as left associativeAdd the RIGHT_ASSOCIATIVE guard
12+3 gives 1 2 3 +Iterating over characters instead of tokensUse tokenize(), which groups digit runs
Result is a list, not a stringoutput is a list of tokens" ".join(output)
2 ^ 3 ^ 2 evaluates to 81Operands popped in the wrong orderPop the right operand first

Frequently asked questions

How do you convert infix to postfix in Python?

Use a list as a stack. Scan the tokens left to right: append operands to the output, push (, and on ) pop to the output until the matching (. For an operator, pop while the top of the stack has higher precedence — or equal precedence and the incoming operator is left associative — then push it. Finally pop whatever remains and " ".join() the output.

Which data structure should I use for the stack in Python?

A plain list. append() and pop() operate on the end in O(1), and stack[-1] is the peek. A custom Stack class adds nothing, and collections.deque is only worth it when you also need fast operations at the front, which this algorithm never does.

Does this handle multi-digit numbers and variable names?

Yes. tokenize() consumes a whole run of alphanumeric characters, underscores and dots as a single operand, so rate * 12 + 250 and 3.5 * 2 both work. Programs that loop over the string one character at a time break on these.

Can I use eval() instead?

Not safely. eval() executes whatever it is given, so any expression coming from a user is a code-execution hole. It also cannot show you the postfix form or the stack, which is the entire point of the exercise.

How do I convert infix to prefix in Python instead?

Reverse the token list and swap every bracket, run the same routine with the associativity test flipped so left-associative operators pop only on strictly higher precedence, then reverse the result.

What Python version does this need?

Python 3.9 or newer, only because of the list[str] annotations. To run it on 3.7 or 3.8, either delete the annotations or add from __future__ import annotations at the top of the file.