SkillAgentSearch skills...

Rpnpy

A reverse-Polish notation calculator for Python

Install / Use

/learn @terrycojones/Rpnpy
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

rpnpy - a reverse-Polish notation calculator for Python

PyPI Python Version Downloads License: MIT

rpnpy is a reverse-Polish notation (RPN) calculator for Python.

The aim is to emulate the operation of early Hewlett-Packard calculators, but generalized by allowing programming in Python, providing access to useful Python functions, and to allow anything to be on the stack.

See the <a href="#background">Background</a> section if you're interested to read more about why I wrote this.

Features

rpnpy

  • provides typical numeric calculator operations.
  • can read commands from the command line, from standard input, or from files.
  • has a simple terminal REPL interface and a text-based user interface (TUI) with clickable buttons for digits and common operations and a display of the stack and variables.
  • provides for input/output using engineering notation, e.g., 12K.
  • provides direct access to over 400 Python functions, pre-imported from the builtins, math, operator, functools, and decimal modules.
  • is programmable. You can write your own Python functions, to be loaded from a start-up file.
  • allows you to put Python data structures, and other objects and functions onto the stack and operate on them.
  • uses readline to keep a command history within and between sessions.
  • is compatible with Python 3.9 through 3.14.

Installation

$ pip install rpnpy. Or if you have uv you can just run uvx rpnpy.

This will install an rpnpy command (you might want to make a more convenient / shorter shell alias for it).

Terminal User Interface

You can run rpnpy with the --tui option to have it present a TUI with buttons, and a stack and variables display.

<img src="https://raw.githubusercontent.com/terrycojones/rpnpy/master/images/tui.png" width=800></img>

You can still use your keyboard when using the TUI. Use ENTER to send your inputs to the calculator.

Use --theme to choose a (Textual) theme. Currently available theme names are catppuccin-latte, catppuccin-mocha, dracula, flexoki, gruvbox, monokai, nord, solarized-light, textual-ansi, textual-dark, textual-light, and tokyo-night. Run rpnpy --help to see the definitive list.

Click the line splitting "off" button at the bottom to toggle line splitting (<a href="#splitting">see below</a> for details).

Example rpnpy sessions

Before getting more formal in describing how to use rpnpy, here are some example command line computations to give you the flavor.

(BTW, I set my shell up to alias pc (Python calculator) for rpnpy to minimize typing and be a bit more like dc. But I'll use rpnpy in the examples below.)

# Add two numbers. The stack is printed after all commands are run.
$ rpnpy 4 5 +
9

# Do the same thing, but read from standard input (all the commands below
# could also be run in this way).
$ echo 4 5 + | rpnpy
9

# Sine of 90 degrees (note that Python's sin function operates on
# radians). The commands are in quotes so the shell doesn't expand the '*'.
$ rpnpy '90 pi 180 / * sin'
1.0

# Same thing, different quoting.
$ rpnpy 90 pi 180 / \* sin
1.0

# Same thing, use 'mul' instead of '*'.
$ rpnpy 90 pi 180 / mul sin
1.0

# Area of a circle radius 10
$ rpnpy 'pi 10 10 * *'
314.1592653589793

# Equivalently, using ':2' to push 10 onto the stack twice.
$ rpnpy 'pi 10:2 * *'
314.1592653589793

# Equivalently, using 'dup' to duplicate the 10 and 'mul' instead of '*'
$ rpnpy pi 10 dup mul mul
314.1592653589793

Function calling argument push order

On a regular RPN calculator you would do what we normally think of as an infix operation such as 5 - 4 by pushing 5 onto the stack, then pushing 4, and finally running the - function. The operator is taken out of middle and given at the end and the original infix order of the arguments is the order you push them onto the stack. Of course this doesn't make any difference for commutative operations like + and *, but is important for / and -.

In Python we have various functions like map, filter, functools.reduce, and the long-ago deprecated Python 2 apply function. These are typically thought of as having a prefix or Polish notation signature, accepting a function followed by an iterable. E.g., map(function, iterable).

To be consistent, with RPN argument pushing just described for the numeric operations, in the case of (what we normally think of as) prefix functions such as map, we should therefore push the function to be run, then push the iterable, then call map (reduce, filter, etc).

Like this:

$ rpnpy 'str:! [6,7,8] map:i'
['6', '7', '8']

Notes

  1. Here the :! modifier causes the str function to be pushed onto the stack instead of being run, and the :i modifier causes the result of map to be iterated before being added to the stack.
  2. When you run a function (like map or apply) that needs a callable (or a function like join that needs a string) and you don't specify a count (using :3 for example), rpnpy will search the stack for a suitable item and use the first one it finds. It doesn't really have a choice in this case because it doesn't know how many arguments the function (once it is found) will be applied to. This should usually work just fine. You can always use an explicit count (like :3) if not. Note that this situation does not apply if you use the :r modifier (see below) because in that case the callable (or string, in the case of join) will be expected to be on the top of the stack (and its signature can then be examined to know how many arguments to pass it).

You might find it more natural to use map and friends the other way around. I.e., first push the iterable, then push the function to be applied, and then call map. In that case, you can use the :r modifier to tell the calculator to reverse the order of the arguments passed to a function. In the following, we push in the other order and then use map:ir (the i is just to iterate the map result to produce a list).

$ rpnpy '[6,7,8] str:! map:ir'
['6', '7', '8']

Continuing on the map theme, you could instead simply reverse part of the stack before running a function:

$ rpnpy '[6,7,8] str:! reverse map:i'
['6', '7', '8']

The reverse command operates on two stack items by default, but it can take a numeric argument or you can run it with the :* modifier which will cause it to be run on the whole stack:

# Reverse the top 3 stack elements then reverse the whole of the stack.
$ rpnpy '5 6 7 8 reverse:3 reverse:*'
[6, 7, 8, 5]

More examples

# The area of a circle again, but using reduce to do the multiplying.  The
# ':!' modifier tells rpnpy to push the '*' function onto the stack
# instead of immediately running it.
$ rpnpy '*:! [pi,10,10] reduce'
314.1592653589793

# Same thing, but push the numbers individually onto the stack, then the
# ':3' tells reduce to use three stack items. Use 'mul' as an
# alternative to '*'.
$ rpnpy 'mul:! pi 10 dup reduce:3'
314.1592653589793

# Equivalently, using ':*' to tell reduce to use the whole stack.
$ rpnpy '*:! pi 10 dup reduce:*'
314.1592653589793

# If you don't want to push the function for 'reduce' to use onto the stack
# first, use ':r' to tell it to use the top of the stack:
$ rpnpy 'pi 10 dup mul:! reduce:3r'
314.1592653589793

# Push 'True' onto the stack 5 times, turn the whole stack ('*') into a
# list and print it ('p'), then pass that list to 'sum'.
$ rpnpy 'True:5 list:*p sum'
[True, True, True, True, True]
5

# Here's something a bit more long-winded (and totally pointless):
#
# Push 0..9 onto the stack (iterating the result of 'range'.
# call Python's 'reversed' function
# push the 'str' function
# use 'map' to convert the list of digits to strings
# join the string digits with the empty string
# convert the result to an int
# take the square root
# push 3 onto the stack
# call 'round' to round the result to three decimal places
#
# the ':i' modifier (used here twice) causes the value from the command
# to be iterated and the result to be put on the stack as a single list.
# It's a convenient way to iterate over a generator, a range, a map,
# dictionary keys, etc.

$ rpnpy 'range(10):i reversed str:! map:ir "" join:r int sqrt 3 round:2'
99380.799

The :2 on the round call tells it to use two arguments from the stack (round uses one by default in rpnpy).

The :r on the map call makes it look for the function to run on the top of the stack, rather than searching up the stack to find it. If you think further in advance, you can push the function first:

$ rpnpy 'str:! range(10):i reversed map:i "" join:r int sqrt 3 round:2'
99380.799

The same goes for the string used by join: it could have been pushed first, and then there would be no need for the :r on the join:

$ rpnpy
View on GitHub
GitHub Stars23
CategoryDevelopment
Updated1mo ago
Forks8

Languages

Python

Security Score

75/100

Audited on Feb 16, 2026

No findings