npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rapydscript-ns

v0.9.2

Published

Pythonic JavaScript that doesn't suck

Readme

RapydScript

Build Status Current Release Known Vulnerabilities

RapydScript is a pre-compiler for Javascript that uses syntax identical to modern Python. It transpiles to native JS (with source maps) that reads like your Python code, but runs natively in the browser or node. Try RapydScript-ns live via an in-browser REPL!

This is a fork of the original RapydScript that adds many new features. The most notable change is that all the Python features that are optional in RapydScript are now enabled by default.

Contents

What is RapydScript?

RapydScript (pronounced 'RapidScript') is a pre-compiler for JavaScript, similar to CoffeeScript, but with cleaner, more readable syntax. The syntax is almost identical to Python, but RapydScript has a focus on performance and interoperability with external JavaScript libraries. This means that the JavaScript that RapydScript generates is performant and quite close to hand- written JavaScript.

RapydScript allows to write your front-end in Python without the overhead that other similar frameworks introduce - the performance is the same as with pure JavaScript. To those familiar with CoffeeScript, RapydScript is similar, but inspired by Python's readability rather than Ruby's cleverness. To those familiar with Pyjamas, RapydScript brings many of the same features and support for Python syntax without the same overhead. RapydScript combines the best features of Python as well as JavaScript, bringing you features most other Pythonic JavaScript replacements overlook. Here are a few features of RapydScript:

  • classes that work and feel similar to Python
  • an import system for modules/packages that works just like Python's
  • optional function arguments that work similar to Python
  • inheritance system that's both, more powerful than Python and cleaner than JavaScript
  • support for object literals with anonymous functions, like in JavaScript
  • ability to invoke any JavaScript/DOM object/function/method as if it's part of the same framework, without the need for special syntax
  • variable and object scoping that make sense (no need for repetitive 'var' or 'new' keywords)
  • ability to use both, Python's methods/functions and JavaScript's alternatives
  • similar to above, ability to use both, Python's and JavaScript's tutorials (as well as widgets)
  • it's self-hosting, that means the compiler is itself written in RapydScript and compiles into JavaScript

Installation

Try RapydScript-ns live via an in-browser REPL!

First make sure you have installed the latest version of node.js (You may need to restart your computer after this step).

From NPM for use as a command line app:

npm install rapydscript-ns -g

From NPM for use in your own node project:

npm install rapydscript-ns

From Git:

git clone https://github.com/ficocelliguy/rapydscript-ns.git
cd rapydscript-ns
sudo npm link .
npm install  # This will automatically install the dependencies for RapydScript

If you're using OSX, you can probably use the same commands (let me know if that's not the case). If you're using Windows, you should be able to follow similar commands after installing node.js, npm and git on your system.

Compilation

Once you have installed RapydScript, compiling your application is as simple as running the following command:

rapydscript [options] <location of main file>

By default this will dump the output to STDOUT, but you can specify the output file using --output option. The generated file can then be referenced in your html page the same way as you would with a typical JavaScript file. If you're only using RapydScript for classes and functions, then you're all set. For more help, use rapydscript -h.

Getting Started

RapydScript comes with its own Read-Eval-Print-Loop (REPL). Just run rapydscript without any arguments to get started trying out the code snippets below.

Like JavaScript, RapydScript can be used to create anything from a quick function to a complex web-app. RapydScript can access anything regular JavaScript can, in the same manner. Let's say we want to write a function that greets us with a "Hello World" pop-up. The following code will do it:

def greet():
	alert("Hello World!")

Once compiled, the above code will turn into the following JavaScript:

function greet() {
	alert("Hello World!");
}

Now you can reference this function from other JavaScript or the page itself (using "onclick", for example). For our next example, let's say you want a function that computes factorial of a number:

def factorial(n):
	if n == 0:
		return 1
	return n * factorial(n-1)

Now all we need is to tie it into our page so that it's interactive. Let's add an input field to the page body and a cell for displaying the factorial of the number in the input once the input loses focus.

	<input id="user-input" onblur="computeFactorial()"></input>
	<div id="result"></div>

NOTE: To complement RapydScript, I have also written RapydML (https://bitbucket.org/pyjeon/rapydml), which is a pre-compiler for HTML (just like RapydScript is a pre-compiler for JavaScript).

Now let's implement computeFactorial() function in RapydScript:

def computeFactorial():
	n = document.getElementById("user-input").value
	document.getElementById("result").innerHTML = factorial(n)

Again, notice that we have access to everything JavaScript has access to, including direct DOM manipulation. Once compiled, this function will look like this:

function computeFactorial() {
	var n;
	n = document.getElementById("user-input").value;
	document.getElementById("result").innerHTML = factorial(n);
}

Notice that RapydScript automatically declares variables in local scope when you try to assign to them. This not only makes your code shorter, but saves you from making common JavaScript mistake of overwriting a global. For more information on controlling variable scope, see Scope Control section.

Leveraging other APIs

Aside from Python-like stdlib, RapydScript does not have any of its own APIs. Nor does it need to, there are already good options available that we can leverage instead. If we wanted, for example, to rewrite the above factorial logic using jQuery, we could easily do so:

def computeFactorial():
	n = $("#user-input").val()
	$("#result").text(factorial(n))

Many of these external APIs, however, take object literals as input. Like with JavaScript, you can easily create those with RapydScript, the same way you would create a dictionary in Python:

styles = {
	'background-color':	'#ffe',
	'border-left':		'5px solid #ccc',
	'width':			50,
}

Now you can pass it to jQuery:

$('#element').css(styles)

Another feature of RapydScript is ability to have functions as part of your object literal. JavaScript APIs often take callback/handler functions as part of their input parameters, and RapydScript lets you create such object literal without any quirks/hacks:

params = {
	'width':	50,
	'height':	30,
	'onclick':	def(event):
		alert("you clicked me"),
	'onmouseover':	def(event):
		$(this).css('background', 'red')
	,
	'onmouseout':	def(event):
		# reset the background
		$(this).css('background', '')
}

Note the comma on a new line following a function declaration, it needs to be there to let the compiler know there are more attributes in this object literal, yet it can't go on the same line as the function since it would get parsed as part of the function block. Like Python, however, RapydScript supports new-line shorthand using a ;, which you could use to place the comma on the same line:

hash = {
	'foo':	def():
		print('foo');,
	'bar':	def():
		print('bar')
}

It is because of easy integration with JavaScript's native libraries that RapydScript keeps its own libraries to a minimum.

Anonymous Functions

Like JavaScript, RapydScript allows the use of anonymous functions. In fact, you've already seen the use of anonymous functions in previous section when creating an object literal ('onmouseover' and 'onmouseout' assignments). Unlike Python's lambda, anonymous functions created with def are not limited to a single expression. The following two function declarations are equivalent:

def factorial(n):
	if n == 0:
		return 1
	return n * factorial(n-1)

factorial = def(n):
	if n == 0:
		return 1
	return n * factorial(n-1)

This might not seem like much at first, but if you're familiar with JavaScript, you know that this can be extremely useful to the programmer, especially when dealing with nested functions, which are a bit syntactically awkward in Python (it's not immediately obvious that those can be copied and assigned to other objects). To illustrate the usefulness, let's create a method that creates and returns an element that changes color while the user keeps the mouse pressed on it.

def makeDivThatTurnsGreen():
	div = $('<div></div>')
	turnGreen = def(event):
		div.css('background', 'green')
	div.mousedown(turnGreen)
	resetColor = def(event):
		div.css('background', '')
	div.mouseup(resetColor)
	return div

At first glance, anonymous functions might not seem that useful. We could have easily created nested functions and assigned them instead. By using anonymous functions, however, we can quickly identify that these functions will be bound to a different object. They belong to the div, not the main function that created them, nor the logic that invoked it. The best use case for these is creating an element inside another function/object without getting confused which object the function belongs to.

Additionally, as you already noticed in the previous section, anonymous functions can be used to avoid creating excessive temporary variables and make your code cleaner:

math_ops = {
	'add':	def(a, b): return a+b;,
	'sub':	def(a, b): return a-b;,
	'mul':	def(a, b): return a*b;,
	'div':	def(a, b): return a/b;,
	'roots':	def(a, b, c):
		r = Math.sqrt(b*b - 4*a*c)
		d = 2*a
		return (-b + r)/d, (-b - r)/d
}

Note that the example puts the function header (def()) and content on the same line (function inlining). This is a feature of RapydScript that can be used to make the code cleaner in cases like the example above. You can also use it in longer functions by chaining statements together using ;.

Lambda Expressions

RapydScript supports Python's lambda keyword for creating single-expression anonymous functions inline. The syntax is identical to Python:

lambda arg1, arg2, ...: expression

The body must be a single expression whose value is implicitly returned. For multi-statement bodies, use def instead.

# Simple lambda assigned to a variable
double = lambda x: x * 2
double(5)  # → 10

# Lambda with multiple arguments
add = lambda a, b: a + b
add(3, 4)  # → 7

# Lambda with no arguments
forty_two = lambda: 42

# Lambda with a ternary body
abs_val = lambda x: x if x >= 0 else -x
abs_val(-5)  # → 5

# Lambda used inline (e.g. as a sort key)
nums = [3, 1, 2]
nums.sort(lambda a, b: a - b)
# nums is now [1, 2, 3]

# Lambda with a default argument value
greet = lambda name='world': 'hello ' + name
greet()         # → 'hello world'
greet('alice')  # → 'hello alice'

# Lambda with *args
total = lambda *args: sum(args)
total(1, 2, 3)  # → 6

# Closure: lambda captures variables from the enclosing scope
def make_adder(n):
    return lambda x: x + n
add5 = make_adder(5)
add5(3)   # → 8

# Nested lambdas
mult = lambda x: lambda y: x * y
mult(3)(4)  # → 12

lambda is purely syntactic sugar — lambda x: expr compiles to the same JavaScript as def(x): return expr. Use def when the body spans multiple lines or needs statements.

Decorators

Like Python, RapydScript supports decorators.

def makebold(fn):
	def wrapped():
		return "<b>" + fn() + "</b>"
	return wrapped

def makeitalic(fn):
	def wrapped():
		return "<i>" + fn() + "</i>"
	return wrapped

@makebold
@makeitalic
def hello():
	return "hello world"

hello() # returns "<b><i>hello world</i></b>"

Class decorators are also supported with the caveat that the class properties must be accessed via the prototype property. For example:

def add_x(cls):
	cls.prototype.x = 1

@add_x
class A:
   pass

print(A.x)  # will print 1

Self-Executing Functions

RapydScript wouldn't be useful if it required work-arounds for things that JavaScript handled easily. If you've worked with JavaScript or jQuery before, you've probably seen the following syntax:

(function(args){
	// some logic here
})(args)

This code calls the function immediately after declaring it instead of assigning it to a variable. Python doesn't have any way of doing this. The closest work-around is this:

def tmp(args):
	# some logic here
tmp.__call__(args)

While it's not horrible, it did litter our namespace with a temporary variable. If we have to do this repeatedly, this pattern does get annoying. This is where RapydScript decided to be a little unorthodox and implement the JavaScript-like solution:

(def(args):
	# some logic here
)()

A close cousin of the above is the following code (passing current scope to the function being called):

function(){
	// some logic here
}.call(this);

With RapydScript equivalent of:

def():
	# some logic here
.call(this)

There is also a third alternative, that will pass the arguments as an array:

def(a, b):
	# some logic here
.apply(this, [a, b])

Chaining Blocks

As seen in previous section, RapydScript will bind any lines beginning with . to the outside of the block with the matching indentation. This logic isn't limited to the .call() method, you can use it with .apply() or any other method/property the function has assigned to it. This can be used for jQuery as well:

$(element)
.css('background-color', 'red')
.show()

The only limitation is that the indentation has to match, if you prefer to indent your chained calls, you can still do so by using the \ delimiter:

$(element)\
	.css('background-color', 'red')\
	.show()

This feature handles do/while loops as well:

a = 0
do:
	print(a)
	a += 1
.while a < 1

Function calling with optional arguments

RapydScript supports the same function calling format as Python. You can have named optional arguments, create functions with variable numbers of arguments and variable numbers of named arguments. Some examples will illustrate this best:

	def f1(a, b=2):
	   return [a, b]

	f1(1, 3) == f1(1, b=3) == [1, 3]

	def f2(a, *args):
		return [a, args]

	f2(1, 2, 3) == [1, [2, 3]]

	def f3(a, b=2, **kwargs):
	    return [a, b, kwargs]

	f3(1, b=3, c=4) == [1, 3, {c:4}]

	def f4(*args, **kwargs):
		return [args, kwargs]

	f4(1, 2, 3, a=1, b=2):
		return [[1, 2, 3], {a:1, b:2}]

Positional-only and keyword-only parameters

RapydScript supports Python's / and * parameter separators:

  • / (positional-only separator): parameters listed before / can only be passed positionally — they cannot be named at the call site.
  • * (keyword-only separator): parameters listed after a bare * can only be passed by name — they cannot be passed positionally.
	def greet(name, /, greeting="Hello", *, punctuation="!"):
	    return greeting + ", " + name + punctuation

	greet("Alice")                          # Hello, Alice!
	greet("Bob", greeting="Hi")             # Hi, Bob!
	greet("Carol", punctuation=".")         # Hello, Carol.
	greet("Dave", greeting="Hey", punctuation="?")  # Hey, Dave?

	# name is positional-only: greet(name="Alice") would silently ignore the kwarg
	# punctuation is keyword-only: must be passed as punctuation="."

The two separators can be combined, and each section can have its own default values. All combinations supported by Python 3.8+ are accepted.

RapydScript is lenient: passing a positional-only parameter by keyword will not raise a TypeError at runtime (the named value is silently ignored), and passing a keyword-only parameter positionally will not raise an error either. This is consistent with RapydScript's general approach of favouring interoperability over strict enforcement.

The Monaco language service correctly shows / and * separators in signature help and hover tooltips.

One difference between RapydScript and Python is that RapydScript is not as strict as Python when it comes to validating function arguments. This is both for performance and to make it easier to interoperate with other JavaScript libraries. So if you do not pass enough arguments when calling a function, the extra arguments will be set to undefined instead of raising a TypeError, as in Python. Similarly, when mixing *args and optional arguments, RapydScript will not complain if an optional argument is specified twice.

When creating callbacks to pass to other JavaScript libraries, it is often the case that the external library expects a function that receives an options object as its last argument. There is a convenient decorator in the standard library that makes this easy:

@options_object
def callback(a, b, opt1=default1, opt2=default2):
	console.log(opt1, opt2)

callback(1, 2, {'opt1':'x', 'opt2':'y'})  # will print x, y

Now when you pass callback into the external library and it is called with an object containing options, they will be automatically converted by RapydScript into the names optional parameters you specified in the function definition.

Inferred Tuple Packing/Unpacking

Like Python, RapydScript allows inferred tuple packing/unpacking and assignment. For example, if you wanted to swap two variables, the following is simpler than explicitly declaring a temporary variable:

a, b = b, a

Likewise, if a function returns multiple variables, it's cleaner to say:

a, b, c = fun()

rather than:

tmp = fun()
a = tmp[0]
b = tmp[1]
c = tmp[2]

Since JavaScript doesn't have tuples, RapydScript uses arrays for tuple packing/unpacking behind the scenes, but the functionality stays the same. Note that unpacking only occurs when you're assigning to multiple arguments:

a, b, c = fun()		# gets unpacked
tmp = fun()			# no unpacking, tmp will store an array of length 3

Unpacking can also be done in for loops (which you can read about in later section):

for index, value in enumerate(items):
	print(index+': '+value)

Tuple packing is the reverse operation, and is done to the variables being assigned, rather than the ones being assigned to. This can occur during assignment or function return:

def fun():
	return 1, 2, 3

To summarize packing and unpacking, it's basically just syntax sugar to remove obvious assignment logic that would just litter the code. For example, the swap operation shown in the beginning of this section is equivalent to the following code:

tmp = [b, a]
a = tmp[0]
b = tmp[1]

RapydScript also supports Python's starred assignment (a, *b, c = iterable), which collects the remaining elements into a list. The starred variable can appear at any position — front, middle, or end:

first, *rest = [1, 2, 3, 4]       # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4]        # init=[1, 2, 3], last=4
head, *mid, tail = [1, 2, 3, 4, 5] # head=1, mid=[2, 3, 4], tail=5

Starred assignment works with any iterable, including generators and strings (which are unpacked character by character). The starred variable always receives a list, even if it captures zero elements.

Explicit tuple literals using parentheses work the same as in Python and compile to JavaScript arrays:

empty   = ()            # []
single  = (42,)         # [42]  — trailing comma required for single-element tuple
pair    = (1, 2)        # [1, 2]
triple  = ('a', 'b', 'c')  # ['a', 'b', 'c']
nested  = ((1, 2), (3, 4))  # [[1, 2], [3, 4]]

A parenthesised expression without a trailing comma is not a tuple — (x) is just x. Add a comma to make it one: (x,).

Tuple literals work naturally everywhere arrays do: as return values, function arguments, in isinstance checks, and in destructuring assignments:

def bounding_box(points):
    return (min(p[0] for p in points), max(p[0] for p in points))

ok  = isinstance(value, (int, str))   # tuple of types

(a, b), c = (1, 2), 3

Operators and keywords

RapydScript uses the python form for operators and keywords. Below is the mapping from RapydScript to JavaScript.

Keywords:

RapydScript		JavaScript

None			null
False			false
True			true
...			ρσ_Ellipsis (the Ellipsis singleton)
undefined		undefined
this			this

Operators:

RapydScript		JavaScript

and				&&
or				||
not				!
is				===
is not			!==
+=1				++
-=1				--
**				Math.pow()
**=				x = Math.pow(x, y)

All Python augmented assignment operators are supported: +=, -=, *=, /=, //=, **=, %=, >>=, <<=, |=, ^=, &=.

Admittedly, is is not exactly the same thing in Python as === in JavaScript, but JavaScript is quirky when it comes to comparing objects anyway.

Literal JavaScript

In rare cases RapydScript might not allow you to do what you need to, and you need access to pure JavaScript, this is particularly useful for performance optimizations in inner loops. When that's the case, you can use a verbatim string literal. That is simply a normal RapydScript string prefixed with the v character. Code inside a verbatim string literal is not a sandbox, you can still interact with it from normal RapydScript:

v'a = {foo: "bar", baz: 1};'
print(a.foo)	# prints "bar"

for v'i = 0; i < arr.length; i++':
   print (arr[i])

Containers (lists/sets/dicts)

Lists

Lists in RapydScript are almost identical to lists in Python, but are also native JavaScript arrays. The sort() and pop() methods behave exactly as in Python: sort() performs a numeric sort (in-place, with optional key and reverse arguments) and pop() performs a bounds-checked pop (raises IndexError for out-of-bounds indices). If you need the native JavaScript behavior for interop with external JS libraries, use jssort() (lexicographic sort) and jspop() (no bounds check, always pops the last element). The old pysort() and pypop() names are kept as backward-compatible aliases.

Note that even list literals in RapydScript create Python-like list objects, and you can also use the builtin list() function to create lists from other iterable objects, just as you would in Python. You can create a RapydScript list from a plain native JavaScript array by using the list_wrap() function, like this:

a = v'[1, 2]'
pya = list_wrap(a)
 # Now pya is a python like list object that satisfies pya === a

List Concatenation

The + operator concatenates two lists and returns a new list, exactly as in Python:

a = [1, 2]
b = [3, 4]
c = a + b   # [1, 2, 3, 4]  — a and b are unchanged

The += operator extends a list in-place (the original list object is mutated):

a = [1, 2]
ref = a      # ref and a point to the same list
a += [3, 4]  # mutates a in-place
print(ref)   # [1, 2, 3, 4]  — ref sees the update

No special flag is required. The + operator compiles to a lightweight helper (ρσ_list_add) that uses Array.concat for lists and falls back to native JS + for numbers and strings.

Sets

Sets in RapydScript are identical to those in python. You can create them using set literals or comprehensions and all set operations are supported. You can store any object in a set. For primitive types (strings, numbers) the value is used for equality; for class instances, object identity (is) is used by default unless the class defines a __hash__ method.

Note that sets are not a subclass of the ES 6 JavaScript Set object, however, they do use this object as a backend, when available. You can create a set from any enumerable container, like you would in python

s = set(list or other set or string)

You can also wrap an existing JavaScript Set object efficiently, without creating a copy with:

js_set = Set()
py_set = set_wrap(js_set)

Note that using non-primitive objects as set members does not behave the same way as in Python. For example:

a = [1, 2]
s = {a}
a in s  # True
[1, 2] in s # False

This is because list identity (not value) determines set membership for mutable objects. Define __hash__ on your own classes to control set/dict membership.

Dicts

dicts are the most different in RapydScript, from Python. This is because RapydScript uses the JavaScript Object as a dict, for compatibility with external JavaScript libraries and performance. This means there are several differences between RapydScript dicts and Python dicts.

- You can only use primitive types (strings/numbers) as keys in the dict
- If you use numbers as keys, they are auto-converted to strings
- You can access the keys of the dict as attributes of the dict object
- Trying to access a non-existent key returns ``undefined`` instead of
  raising a KeyError
- dict objects do not have the same methods as python dict objects:
  ``items(), keys(), values(), get(), pop(), etc.`` You can however use
  RapydScript dict objects in ```for..in``` loops.

Fortunately, there is a builtin dict type that behaves just like Python's dict with all the same methods. The dict_literals and overload_getitem flags are on by default, so dict literals and the [] operator already behave like Python:

a = {1:1, 2:2}
a[1]  # == 1
a[3] = 3
list(a.keys()) == [1, 2, 3]
a['3'] # raises a KeyError as this is a proper python dict, not a JavaScript object

These are scoped flags — local to the scope where they appear. You can disable them for a region of code using the no_ prefix:

a = {1:1, 2:2}
isinstance(a, dict) == True
from __python__ import no_dict_literals, no_overload_getitem
a = {1:1, 2:2}
isinstance(a, dict) == False # a is a normal JavaScript object

List spread literals

RapydScript supports Python's *expr spread syntax inside list literals. One or more *expr items can appear anywhere, interleaved with ordinary elements:

a = [1, 2, 3]
b = [4, 5]

# Spread at the end
result = [0, *a]            # [0, 1, 2, 3]

# Spread in the middle
result = [0, *a, *b, 6]     # [0, 1, 2, 3, 4, 5, 6]

# Copy a list
copy = [*a]                 # [1, 2, 3]

# Unpack a string
chars = [*'hello']          # ['h', 'e', 'l', 'l', 'o']

Spread works on any iterable (lists, strings, generators, range()). The result is always a new Python list. Translates to JavaScript's [...expr] spread syntax.

Set spread literals

The same *expr syntax works inside set literals {...}:

a = [1, 2, 3]
b = [3, 4, 5]

s = {*a, *b}                # set([1, 2, 3, 4, 5]) — duplicates removed
s2 = {*a, 10}               # set([1, 2, 3, 10])

Translates to ρσ_set([...a, ...b]).

**expr in function calls

**expr in a function call now accepts any expression, not just a plain variable name:

def f(x=0, y=0):
    return x + y

opts = {'x': 10, 'y': 20}
f(**opts)                   # 30  (variable — always worked)
f(**{'x': 1, 'y': 2})      # 3   (dict literal)
f(**cfg.defaults)           # uses attribute access result
f(**get_opts())             # uses function call result

Dict merge literals

RapydScript supports Python's {**d1, **d2} dict merge (spread) syntax. One or more **expr items can appear anywhere inside a {...} literal, interleaved with ordinary key: value pairs:

defaults = {'color': 'blue', 'size': 10}
overrides = {'size': 20}

# Merge two dicts — later items win
merged = {**defaults, **overrides}
# merged == {'color': 'blue', 'size': 20}

# Mix spread with literal key-value pairs
result = {**defaults, 'weight': 5}
# result == {'color': 'blue', 'size': 10, 'weight': 5}

This works for both plain JavaScript-object dicts and Python dict objects (dict_literals is on by default):

pd1 = {'a': 1}
pd2 = {'b': 2}
merged = {**pd1, **pd2}   # isinstance(merged, dict) == True

The spread items are translated using Object.assign for plain JS objects and dict.update() for Python dicts.

Dict merge operator | and |= (Python 3.9+)

Python dicts support the | (merge) and |= (update in-place) operators (requires overload_operators and dict_literals, both on by default):

d1 = {'x': 1, 'y': 2}
d2 = {'y': 99, 'z': 3}

# Create a new merged dict — right-side values win on key conflict
merged = d1 | d2   # {'x': 1, 'y': 99, 'z': 3}

# Update d1 in place
d1 |= d2           # d1 is now {'x': 1, 'y': 99, 'z': 3}

d1 | d2 creates a new dict (neither operand is mutated). d1 |= d2 merges d2 into d1 and returns d1.

Without overload_operators the | symbol is bitwise OR — use {**d1, **d2} spread syntax as an alternative if the flag is disabled.

Arithmetic operator overloading

RapydScript supports Python-style arithmetic operator overloading via the overload_operators flag, which is on by default:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __neg__(self):
        return Vector(-self.x, -self.y)

a = Vector(1, 2)
b = Vector(3, 4)
c = a + b   # calls a.__add__(b)  →  Vector(4, 6)
d = -a      # calls a.__neg__()   →  Vector(-1, -2)

The supported dunder methods are:

| Expression | Forward method | Reflected method | |------------|-----------------|-------------------| | a + b | __add__ | __radd__ | | a - b | __sub__ | __rsub__ | | a * b | __mul__ | __rmul__ | | a / b | __truediv__ | __rtruediv__ | | a // b | __floordiv__ | __rfloordiv__ | | a % b | __mod__ | __rmod__ | | a ** b | __pow__ | __rpow__ | | a & b | __and__ | __rand__ | | a \| b | __or__ | __ror__ | | a ^ b | __xor__ | __rxor__ | | a << b | __lshift__ | __rlshift__ | | a >> b | __rshift__ | __rrshift__ | | -a | __neg__ | | | +a | __pos__ | | | ~a | __invert__ | |

Augmented assignment (+=, -=, etc.) first tries the in-place method (__iadd__, __isub__, …) and then falls back to the binary method.

If neither operand defines the relevant dunder method, the operation enforces Python-style type compatibility before falling back to the native JavaScript operator:

  • number ± number → allowed (including bool, which is treated as an integer subclass)
  • str + str → allowed
  • str * int and list * int → allowed (and also with bool in place of int)
  • Anything else raises TypeError with a Python-style message, e.g.:
1 + 'x'       # TypeError: unsupported operand type(s) for +: 'int' and 'str'
'a' - 1       # TypeError: unsupported operand type(s) for -: 'str' and 'int'
[1] + 'b'     # TypeError: unsupported operand type(s) for +: 'list' and 'str'

This type-checking is controlled by the strict_arithmetic flag, which is on by default when overload_operators is active. To revert to JavaScript's silent coercion behaviour (e.g. 1 + 'x''1x') without disabling dunder dispatch, use:

from __python__ import no_strict_arithmetic

When overload_operators is disabled (from __python__ import no_overload_operators) the operators compile directly to JavaScript and no type checking is performed.

When overload_operators is active, string and list repetition with * works just like Python:

'ha' * 3      # 'hahaha'
3 * 'ha'      # 'hahaha'
[0] * 4       # [0, 0, 0, 0]
[1, 2] * 2    # [1, 2, 1, 2]

Because the dispatch adds one or two property lookups per operation, you can disable it in scopes where it is not needed with from __python__ import no_overload_operators.

The collections.Counter class defines __add__, __sub__, __or__, and __and__. With overload_operators you can use the natural operator syntax:

from collections import Counter

c1 = Counter('aab')
c2 = Counter('ab')
c3 = c1 + c2   # {'a': 3, 'b': 2}
c4 = c1 - c2   # {'a': 1}
c5 = c1 | c2   # union  (max) → {'a': 2, 'b': 1}
c6 = c1 & c2   # intersection (min) → {'a': 1, 'b': 1}

Container comparisons

Container equality (the == and != operators) work for lists and sets and RapydScript dicts (but not arbitrary javascript objects). You can also define the __eq__(self, other) method in your classes to have these operators work for your own types.

The ordering operators <, >, <=, >= dispatch to Python-style dunder methods and compare lists lexicographically — just like Python:

from __python__ import overload_operators  # on by default

# List comparison — lexicographic order
assert [1, 2] < [1, 3]     # True  (first differing element: 2 < 3)
assert [1, 2] < [1, 2, 0]  # True  (prefix is smaller)
assert [2] > [1, 99]       # True  (first element dominates)

# Works with custom __lt__ / __gt__ / __le__ / __ge__ on objects
class Version:
    def __init__(self, major, minor):
        self.major = major
        self.minor = minor
    def __lt__(self, other):
        return (self.major, self.minor) < (other.major, other.minor)

v1 = Version(1, 5)
v2 = Version(2, 0)
assert v1 < v2   # dispatches to __lt__

# Incompatible types raise TypeError, just like Python
try:
    result = [1] < 42
except TypeError as e:
    print(e)   # '<' not supported between instances of 'list' and 'int'

Chained comparisons work just like Python — each middle operand is evaluated only once:

# All of these work correctly, including mixed-direction chains
assert 1 < 2 < 3      # True
assert 1 < 2 > 0      # True  (1<2 AND 2>0)
assert [1] < [2] < [3]   # True  (lexicographic chain)

Python Truthiness and __bool__

RapydScript uses Python truthiness semantics by default (truthiness is on by default):

When this flag is active:

  • Empty containers are falsy: [], {}, set(), '', 0, None are all falsy.
  • __bool__ is dispatched: objects with a __bool__ method control their truthiness.
  • __len__ is used: objects with __len__ are falsy when len(obj) == 0.
  • and/or return operand values (not booleans), just like Python.
  • All condition positions (if, while, assert, not, ternary) use Python semantics.
class Empty:
    def __bool__(self): return False

if not []:          # True — [] is falsy
    print('empty')

x = [] or 'default'   # x == 'default'
y = [1] or 'default'  # y == [1]
z = [1] and 'ok'      # z == 'ok'

The flag is scoped — it applies until the end of the enclosing function or class body. Use from __python__ import no_truthiness to disable it in a sub-scope where JavaScript truthiness is needed.

Callable Objects (__call__)

Any class that defines __call__ can be invoked directly with obj(args), just like Python callable objects:

class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    def __call__(self, x):
        return self.factor * x

triple = Multiplier(3)
triple(7)   # 21 — dispatches to triple.__call__(7)

callable(obj) returns True when __call__ is defined. The dispatch is automatic for all direct function-call expressions that are simple names (i.e. not method accesses like obj.method()).

frozenset

RapydScript provides a full frozenset builtin — an immutable, unordered collection of unique elements, identical to Python's frozenset.

fs = frozenset([1, 2, 3])
len(fs)          # 3
2 in fs          # True
isinstance(fs, frozenset)  # True

# Set operations return new frozensets
a = frozenset([1, 2, 3])
b = frozenset([2, 3, 4])
a.union(b)                 # frozenset({1, 2, 3, 4})
a.intersection(b)          # frozenset({2, 3})
a.difference(b)            # frozenset({1})
a.symmetric_difference(b)  # frozenset({1, 4})

a.issubset(frozenset([1, 2, 3, 4]))   # True
a.issuperset(frozenset([1, 2]))        # True
a.isdisjoint(frozenset([5, 6]))        # True

# Compares equal to a set with the same elements
frozenset([1, 2]).__eq__({1, 2})  # True

Mutation methods (add, remove, discard, clear, update) are not present on frozenset instances, enforcing immutability at the API level. frozenset objects can be iterated and copied with .copy().

bytes and bytearray

RapydScript provides bytes (immutable) and bytearray (mutable) builtins that match Python's semantics and are backed by plain JS arrays of integers in the range 0–255.

b'...' bytes literals

RapydScript supports Python b'...' bytes literal syntax. The prefix may be b or B (and rb/br for raw bytes where backslash sequences are not interpreted). Adjacent bytes literals are automatically concatenated, just like adjacent string literals.

b'Hello'              # bytes([72, 101, 108, 108, 111])
b'\x00\xff'           # bytes([0, 255])  — hex escape sequences work
b'\n\t\r'             # bytes([10, 9, 13]) — control-char escapes work
b'foo' b'bar'         # bytes([102, 111, 111, 98, 97, 114])  — concatenation
rb'\n\t'              # bytes([92, 110, 92, 116])  — raw: backslashes literal
B'ABC'                # bytes([65, 66, 67])  — uppercase B also accepted

Each b'...' literal is compiled to a bytes(str, 'latin-1') call, so the full bytes API is available on the result.

Construction

bytes()                      # empty bytes
bytes(4)                     # b'\x00\x00\x00\x00'  (4 zero bytes)
b'\x00\x00\x00\x00'          # same — bytes literal syntax
bytes([72, 101, 108, 111])   # b'Hello'
b'Hell\x6f'                  # same — mix of ASCII and hex escapes
bytes('Hello', 'utf-8')      # encode a string
bytes('ABC', 'ascii')        # ASCII / latin-1 encoding also accepted
bytes.fromhex('48656c6c6f')  # from hex string → b'Hello'

bytearray()                  # empty mutable byte sequence
bytearray(3)                 # bytearray(b'\x00\x00\x00')
bytearray([1, 2, 3])         # from list of ints
bytearray('Hi', 'utf-8')     # from string
bytearray(some_bytes)        # mutable copy of a bytes object

Uint8Array values may also be passed as the source argument.

Common operations (both bytes and bytearray)

b = bytes('Hello', 'utf-8')

len(b)                        # 5
b[0]                          # 72  (integer)
b[-1]                         # 111
b[1:4]                        # bytes([101, 108, 108])  (slice → new bytes)
b[::2]                        # every other byte

b + bytes([33])               # concatenate → b'Hello!'
b * 2                         # repeat     → b'HelloHello'
72 in b                       # True  (integer membership)
bytes([101, 108]) in b        # True  (subsequence membership)
b == bytes([72, 101, 108, 108, 111])  # True

b.hex()                       # '48656c6c6f'
b.hex(':', 2)                 # '48:65:6c:6c:6f'  (separator every 2 bytes)
b.decode('utf-8')             # 'Hello'
b.decode('ascii')             # works for ASCII-range bytes

b.find(bytes([108, 108]))     # 2
b.index(101)                  # 1
b.rfind(108)                  # 3
b.count(108)                  # 2
b.startswith(bytes([72]))     # True
b.endswith(bytes([111]))      # True
b.split(bytes([108]))         # [b'He', b'', b'o']
b.replace(bytes([108]), bytes([76]))  # b'HeLLo'
b.strip()                     # strip leading/trailing whitespace bytes
b.upper()                     # b'HELLO'
b.lower()                     # b'hello'
bytes(b' ').join([bytes('a', 'ascii'), bytes('b', 'ascii')])  # b'a b'

repr(b)                       # "b'Hello'"
isinstance(b, bytes)          # True
isinstance(bytearray([1]), bytes)  # True  (bytearray is a subclass of bytes)

bytearray-only mutation methods

ba = bytearray([1, 2, 3])
ba[0] = 99                    # item assignment
ba[1:3] = bytes([20, 30])     # slice assignment
ba.append(4)                  # add one byte
ba.extend([5, 6])             # add multiple bytes
ba.insert(0, 0)               # insert at index
ba.pop()                      # remove and return last byte (or ba.pop(i))
ba.remove(20)                 # remove first occurrence of value
ba.reverse()                  # reverse in place
ba.clear()                    # remove all bytes
ba += bytearray([7, 8])       # in-place concatenation

issubclass

issubclass(cls, classinfo) checks whether a class is a subclass of another class (or any class in a tuple of classes). Every class is considered a subclass of itself.

class Animal: pass
class Dog(Animal): pass
class Poodle(Dog): pass
class Cat(Animal): pass

issubclass(Dog, Animal)            # True
issubclass(Poodle, Animal)         # True — transitive
issubclass(Poodle, Dog)            # True
issubclass(Dog, Dog)               # True — a class is its own subclass
issubclass(Cat, Dog)               # False
issubclass(Animal, Dog)            # False — parent is not a subclass of child

# tuple form — True if cls is a subclass of any entry
issubclass(Dog, (Cat, Animal))     # True
issubclass(Poodle, (Cat, Dog))     # True

TypeError is raised when either argument is not a class.

hash

hash(obj) returns an integer hash value for an object, following Python semantics:

| Type | Hash rule | |---|---| | None | 0 | | bool | 1 for True, 0 for False | | int / whole float | the integer value itself | | other float | derived from the bit pattern | | str | djb2 algorithm — stable within a process | | object with __hash__ | dispatches to __hash__() | | class instance (no __hash__) | stable identity hash (assigned on first call) | | class with __eq__ but no __hash__ | TypeError (unhashable — Python semantics) | | list | TypeError: unhashable type: 'list' | | set | TypeError: unhashable type: 'set' | | dict | TypeError: unhashable type: 'dict' |

hash(None)        # 0
hash(True)        # 1
hash(42)          # 42
hash(3.0)         # 3   (whole float → same as int)
hash('hello')     # stable integer

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __hash__(self):
        return self.x * 31 + self.y

hash(Point(1, 2))  # 33

# Python semantics: __eq__ without __hash__ → unhashable
class Bar:
    def __eq__(self, other):
        return True
hash(Bar())  # TypeError: unhashable type: 'Bar'

eval and exec

Both eval and exec are supported with Python-compatible signatures. String literals passed to them are treated as RapydScript source code: the compiler parses and transpiles the string at compile time, so you write RapydScript (not raw JavaScript) inside the quotes — just like Python's eval/exec take Python source strings.

eval(expr[, globals[, locals]])

  • One argument — the compiled expression is passed to the native JS eval, giving direct scope access to module-level variables:

    result = eval("1 + 2")               # 3
    x = 7
    sq = eval("x * x")                   # 49  (x is in scope)
  • Two or three arguments — uses the Function constructor with explicit variable bindings. locals override globals when both are given:

    eval("x + y", {"x": 10, "y": 5})          # 15
    eval("x", {"x": 1}, {"x": 99})             # 99  (local overrides global)

exec(code[, globals[, locals]])

Executes a RapydScript code string and always returns None, like Python's exec.

  • One argument — the compiled code runs via native eval:

    exec("print('hi')")           # prints hi
    exec("_x = 42")               # _x is discarded after exec returns
  • Two or three arguments — uses the Function constructor. Mutable objects (arrays, dicts) passed in globals are accessible by reference, so mutations are visible in the caller:

    log = []
    exec("log.append(1 + 2)", {"log": log})
    print(log[0])    # 3
    
    def add(a, b): log.append(a + b);
    exec("fn(10, 7)", {"fn": add, "log": log})
    print(log[1])    # 17

Note: Because strings are compiled at compile time, only string literals are transformed — dynamic strings assembled at runtime are passed through unchanged. exec(code) cannot modify the caller's local variables, matching Python 3 semantics.

vars, locals, and globals

vars(obj)

Returns a dict snapshot of the object's own instance attributes, mirroring Python's obj.__dict__. Internal RapydScript properties (prefixed ρσ) are excluded automatically. Mutating the returned dict does not affect the original object.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(3, 4)
d = vars(p)
print(d['x'], d['y'])   # 3 4
print(list(d.keys()))   # ['x', 'y']

vars() and locals()

Both return an empty dict. JavaScript has no runtime mechanism for introspecting the local call-frame's variables, so a faithful implementation is not possible. Use them as placeholders in patterns that require a dict, or pass explicit dicts where you need named-value lookup.

loc = locals()   # {}
v   = vars()     # {}

globals()

Returns a dict snapshot of the JS global object (globalThis / window / global). Module-level RapydScript variables compiled inside an IIFE or module wrapper will not appear here; use a shared plain dict for that pattern instead.

g = globals()
# g contains JS runtime globals such as Math, console, etc.
print('Math' in g)   # True (in a browser or Node context)

Complex numbers

RapydScript supports Python's complex number type via the complex builtin and the j/J imaginary literal suffix.

# Imaginary literal suffix
z = 4j          # complex(0, 4)
w = 3 + 4j      # complex(3, 4)   — parsed as 3 + complex(0, 4)

# Constructor
z1 = complex(3, 4)    # real=3, imag=4
z2 = complex(5)       # real=5, imag=0
z3 = complex()        # 0+0j
z4 = complex('2-3j')  # string parsing

# Attributes
print(z1.real)   # 3
print(z1.imag)   # 4

# Methods
print(z1.conjugate())   # (3-4j)
print(abs(z1))          # 5.0  — dispatches __abs__

# Arithmetic (requires overload_operators, which is on by default)
from __python__ import overload_operators
print(z1 + z2)   # (8+4j)
print(z1 - z2)   # (-2+4j)
print(z1 * z2)   # (15+20j)
print(z1 / z2)   # (0.6+0.8j)

# Truthiness, repr, isinstance
print(bool(complex(0, 0)))   # False
print(repr(z1))              # (3+4j)
print(isinstance(z1, complex))  # True

The j/J suffix is handled at the tokenizer level: 4j is parsed into an AST_Call(complex, 0, 4) node, so it composes naturally with all other expressions. Mixed expressions like 3 + 4j work without overload_operators because ρσ_list_add dispatches __radd__ on the right operand.

Attribute-Access Dunders

RapydScript supports the four Python attribute-interception hooks: __getattr__, __setattr__, __delattr__, and __getattribute__. When a class defines any of them, instances are automatically wrapped in a JavaScript Proxy that routes attribute access through the hooks — including accesses that occur inside __init__.

| Hook | When called | |---|---| | __getattr__(self, name) | Fallback — only called when normal lookup finds nothing | | __setattr__(self, name, value) | Every attribute assignment (including self.x = … in __init__) | | __delattr__(self, name) | Every del obj.attr | | __getattribute__(self, name) | Every attribute read (overrides normal lookup) |

To bypass the hooks from within the hook itself (avoiding infinite recursion), use the object.* bypass functions:

| Python idiom | Compiled form | Effect | |---|---|---| | object.__setattr__(self, name, val) | ρσ_object_setattr(self, name, val) | Set attribute directly, bypassing __setattr__ | | object.__getattribute__(self, name) | ρσ_object_getattr(self, name) | Read attribute directly, bypassing __getattribute__ | | object.__delattr__(self, name) | ρσ_object_delattr(self, name) | Delete attribute directly, bypassing __delattr__ |

Subclasses automatically inherit proxy wrapping from their parent class — if Base defines __getattr__, all Child(Base) instances are also Proxy-wrapped.

class Validated:
    """Reject negative values at assignment time."""
    def __setattr__(self, name, value):
        if jstype(value) is 'number' and value < 0:
            raise ValueError(name + ' must be non-negative')
        object.__setattr__(self, name, value)

v = Validated()
v.x = 5    # ok
v.x = -1   # ValueError: x must be non-negative

class AttrProxy:
    """Log every attribute read."""
    def __init__(self):
        object.__setattr__(self, '_log', [])

    def __getattribute__(self, name):
        self._log.append(name)          # self._log goes through __getattribute__ too!
        return object.__getattribute__(self, name)

Proxy support required — The hooks rely on Proxy, which is available in all modern browsers and Node.js ≥ 6. In environments that lack Proxy the class still works, but the hooks are silently bypassed.

Loops

RapydScript's loops work like Python, not JavaScript. You can't, for example use for(i=0;i<max;i++) syntax. You can, however, loop through arrays using 'for ... in' syntax without worrying about the extra irrelevant attributes regular JavaScript returns.

animals = ['cat', 'dog', 'mouse', 'horse']
for animal in animals:
	print('I have a '+animal)

If you need to use the index in the loop as well, you can do so by using enumerate():

for index, animal in enumerate(animals):
	print("index:"+index, "animal:"+animal)

enumerate() supports an optional start argument (default 0):

for index, animal in enumerate(animals, 1):
	print(str(index) + '. ' + animal)  # 1-based numbering

Like in Python, for loops support an else clause that runs only when the loop completes without hitting a break:

for animal in animals:
    if animal == 'cat':
        print('found a cat')
        break
else:
    print('no cat found')

This is useful for search patterns where you want to take an action only if the searched item was not found.

while loops also support an else clause, which runs when the condition becomes False (i.e. no break was executed):

i = 0
while i < len(items):
    if items[i] == target:
        print('found at', i)
        break
    i += 1
else:
    print('not found')

Like in Python, if you just want the index, you can use range:

for index in range(len(animals)):			# or range(animals.length)
	print("animal "+index+" is a "+animals[index])

When possible, RapydScript will automatically optimize the loop for you into JavaScript's basic syntax, so you're not missing much by not being able to call it directly.

List/Set/Dict Comprehensions

RapydScript also supports comprehensions, using Python syntax. Instead of the following, for example:

myArray = []
for index in range(1,20):
	if index*index % 3 == 0:
		myArray.append(index*index)

You could write this:

myArray = [i*i for i in range(1,20) if i*i%3 == 0]

Similarly for set and dict comprehensions:

myDict = {x:x+1 for x in range(20) if x > 2}
mySet = {i*i for i in range(1,20) if i*i%3 == 0}

Nested comprehensions (multiple for and if clauses) are also supported, using the same syntax as Python:

# Flatten a 2-D list
flat = [x for row in matrix for x in row]

# Cartesian product of two ranges
coords = [[i, j] for i in range(3) for j in range(3)]

# Filter across nested loops
evens = [x for row in [[1,2,3],[4,5,6]] for x in row if x % 2 == 0]

# Nested set/dict comprehensions work too
unique_sums = {x + y for x in range(4) for y in range(4)}

Any number of for clauses may be combined, each optionally followed by one or more if conditions.

Builtin iteration functions: any() and all()

RapydScript supports Python's any() and all() built-in functions with identical semantics. Both work with arrays, strings, iterators, range() objects, and any other iterable.

any(iterable) returns True if at least one element of the iterable is truthy, and False if all elements are falsy or the iterable is empty:

any([False, 0, '', 1])   # True
any([False, 0, None])    # False
any([])                  # False
any(range(3))            # True  (range produces 0, 1, 2 — 1 and 2 are truthy)
any(range(0))            # False (empty range)

all(iterable) returns True only if every element is truthy (or the iterable is empty):

all([1, 2, 3])           # True
all([1, 0, 3])           # False
all([])                  # True  (vacuously true)
all(range(1, 4))         # True  (1, 2, 3 — all truthy)
all(range(0, 3))         # False (range starts at 0, which is falsy)

Both functions short-circuit: any() stops as soon as it finds a truthy element, and all() stops as soon as it finds a falsy element. This makes them efficient even with large or lazy iterables.

They work naturally with list comprehensions for expressive one-liners:

nums = [2, 4, 6, 8]
all([x > 0 for x in nums])   # True — all positive
any([x > 5 for x in nums])   # True — some greater than 5
all([x > 5 for x in nums])   # False — not all greater than 5

Both any() and all() compile to plain JavaScript function calls and are always available without any import.

Strings

For reasons of compatibility with external JavaScript and performance, RapydScript does not make any changes to the native JavaScript string type. However, all the useful Python string methods are available on the builtin str object. This is analogous to how the functions are available in the string module in Python 2.x. For example,

str.strip(' a ') == 'a'
str.split('a b') == ['a', 'b']
str.format('{0:02d} {n}', 1, n=2) == '01 2'
...

The format(value[, spec]) builtin is also supported. It applies the Python format-spec mini-language to a single value — the same mini-language that follows : in f-strings and str.format() fields:

format(42, '08b')     # '00101010'  — zero-padded binary
format(3.14159, '.2f') # '3.14'     — fixed-point
format('hi', '>10')   # '        hi' — right-aligned in 10-char field
format(42)            # '42'        — no spec: same as str(42)

Objects with a __format__ method are dispatched to it in all three contexts — format(obj, spec), str.format('{:spec}', obj), and f'{obj:spec}' — matching Python's protocol exactly. Every user-defined class automatically gets a default __format__ that returns str(self) for an empty spec and raises TypeError for any other spec, just like object.__format__ in Python:

class Money:
    def __init__(self, amount):
        self.amount = amount
    def __str__(self):
        return str(self.amount)
    def __format__(self, spec):
        if spec == 'usd':
            return '$' + str(self.amount)
        return format(self.amount, spec)  # delegate numeric specs

m = Money(42)
format(m, 'usd')          # '$42'
str.format('{:usd}', m)   # '$42'
f'{m:usd}'                # '$42'
f'{m:.2f}'                # '42.00'

The !r, !s, and !a conversion flags apply repr()/str()/repr() to the value before formatting, bypassing __format__ (same as Python).

String predicate methods are also available:

str.isalpha('abc')      # True — all alphabetic
str.isdigit('123')      # True — all digits
str.isalnum('abc123')   # True — alphanumeric
str.isspace('   ')      # True — all whitespace
str.isupper('ABC')      # True
str.islower('abc')      # True
str.isidentifier('my_var')  # True — valid Python identifier

Python 3.9 prefix/suffix removal:

str.removeprefix('HelloWorld', 'Hello')  # 'World'
str.removesuffix('HelloWorld', 'World')  # 'Hello'

Case-folding for locale-insensitive lowercase comparison:

str.casefold('ÄÖÜ') == str.casefold('äöü')  # True (maps to lowercase)

Tab expansion:

str.expandtabs('a\tb', 4)   # 'a   b'  — expand to next 4-space tab stop
str.expandtabs('\t\t', 8)   # '                '  — two full tab stops
str.expandtabs('ab\tc', 4)  # 'ab  c'  — only 2 spaces needed to reach next stop

The optional tabsize argument defaults to 8, matching Python's default. A tabsize of 0 removes all tab characters. Newline (\n) and carriage-return (\r) characters reset the column counter, so each line is expanded independently.

However, if you want to make the python string methods available on string objects, there is a convenience method in the standard library to do so. Use the following code:

from pythonize import strings
strings()

After you call the strings() function, all python string methods will be available on string objects, just as in python. The only caveat is that two methods: split() and replace() are left as the native JavaScript versions, as their behavior is not compatible with that of the python versions. You can control which methods are not copied to the JavaScript String object by passing their names to the strings() function, like this:

strings('split', 'replace', 'find', ...)
# or
strings(None)  # no methods are excluded

One thing to keep in mind is that in JavaScript string are UTF-16, so they behave like strings in narrow builds of Python 2.x. This means that non-BMP unicode characters are represented as surrogate pairs. RapydScript includes some functions to make dealing with non-BMP unicode characters easier:

  • str.uchrs(string, [with_positions]) -- iterate over unicode characters in string, so, for example:

    list(str.uchrs('s🐱a')) == ['s', "🐱", 'a']

    You can also get positions of individual characters:

    list(str.uchrs('s🐱a', True)) == [[0, 's'], [1, "🐱"], [3, 'a']]

    Note that any broken surrogate pairs in the underlying string are returned as the unicode replacement character U+FFFD

  • str.uslice(string, [start, [stop]]) -- get a slice based on unicode character positions, for example:

    str.uslice('s🐱a', 2') == 'a'  # even though a is at index 3 in the native string object
  • str.ulen(string) -- return the number of unicode characters in the string

The Existential Operator

One of the annoying warts of JavaScript is that there are two "null-like" values: undefined and null. So if you want to test if a variable is not null you often have to write a lengthy expression that looks like

(var !== undefined and var !== None)

Simply doing bool(var) will not work because zero and empty strings are also False.

Similarly, if you need to access a chain of properties/keys and dont want a TypeError to be raised, if one of them is undefined/null then you have to do something like:

if a and a.b and a.b.c:
	ans = a.b.c()
else:
	ans = undefined

To ease these irritations, RapydScript borrows the Existential operator from CoffeeScript. This can be used to test if a variable is null-like, with a single character, like this:

yes = True if no? else False
# Which, without the ? operator becomes
yes = True if no is not undefined and no is not None else False

When it comes to long chains, the ? operator will return the expected value if all parts of the chain are ok, but cause the entire chaning to result in undefined if any of its links are null-like. For example:

ans = a?.b?[1]?()
# Which, without the ? operator becomes
ans = undefined
if a is not undefined and a is not None and a.b is not undefined and a.b is not None and jstype(a.b[1]) is 'function':
	ans = a.b[1]()

Finally, you can also use the existential operator as shorthand for the conditional ternary operator, like this:

a = b ? c
# is the same as
a = c if (b is undefined or b is None) else b

Walrus Operator

RapydScript supports the walrus (assignment expression) operator := from Python 3.8 (PEP 572). It assigns a value and returns it as an expression, allowing you to avoid repeating a computation:

# assign and test in a single expression
if m := re.match(r'\d+', line):
    print(m.group())

# drain an iterable in a while loop
while chunk := file.read(8192):
    process(chunk)

# filter and capture in a comprehension
results = [y for x in data if (y := transform(x)) is not None]

The walrus operator binds to the nearest enclosing function or module scope (not the comprehension scope), matching Python semantics.

Ellipsis Literal

RapydScript supports the Python ... (Ellipsis) literal. It compiles to a frozen singleton object ρσ_Ellipsis and is also accessible as Ellipsis (the global name), matching Python behaviour.

Common uses:

# As a placeholder body (like pass)
def stub():
    ...

# As a sentinel / marker value
def process(data, mask=...):
    if mask is ...: