Skip to content

edemaine/coffeescript-for-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Logo: Python snake in CoffeeScript cup CoffeeScript for Python Programmers

See https://edemaine.github.io/coffeescript-for-python/ for a better-formatted version of this document. {: .github-only}

CoffeeScript is a programming language whose syntax is clearly designed to match much of Python (with additional inspirations from Perl, ECMAScript, Ruby, etc.). But most documentation for learning CoffeeScript assumes knowledge of JavaScript (which CoffeeScript compiles to), which is far messier.

This guide attempts to teach CoffeeScript to someone fluent in just Python (such as the typical MIT sophomore), showing the slight tweaks needed to convert Python code into CoffeeScript code. The goal is to make it easy for someone to learn CoffeeScript as a second language after Python, without first learning JavaScript, thereby enabling a Python programmer to also make cool web applications (for example). This should also make it easier to later learn JavaScript as a third language (as the CoffeeScript compiler and documentation provides countless examples relating the two).

This guide is still a work-in-progress, and is not yet complete. Feel free to submit an issue for anything you find lacking or confusing.

Why CoffeeScript instead of Python?

Both Python and CoffeeScript are great languages. The main reason to prefer CoffeeScript is that it compiles to JavaScript, resulting in several advantages:

  1. It can run in any web browser, making it easy to distribute your software for people to play with: just embed it in a web page, and anyone can run it on their desktop computer or smartphone. (This feature is also especially important for web development.) It can also run stand-alone/from a command line (like Python) via Node, which is now the basis for many web servers as part of a complete "web application".
  2. When running in a browser, you gain access to many powerful GUI features in the browser, notably HTML/CSS (e.g., buttons), SVG (vector 2D graphics), Canvas (raster 2D graphics), and WebGL (3D graphics). This makes it really easy to do complex interactive graphics.
  3. It is much faster: Node runs typical CoffeeScript 2-5x faster than the equivalent Python (though this gap narrows if you use PyPy). This performance boost is thanks to extensive optimization, such as just-in-time compilation, because of the intense interest in web applications. Low-level access to typed arrays, WebWorker threads, etc. can make Coffee/JavaScript even faster. For example, this π calculation benchmark shows that well-tuned JavaScript code is around 5x faster than NumPy, 100x faster than CPython, and only around 2x slower than highly optimized C.

An alternative would be to learn a language like RapydScript which is more similar to Python but still compiles directly to JavaScript (like CoffeeScript does).

Major Similarities and Differences

Both Python and CoffeeScript share many features:

They also have some major differences (some better for Python and some better for CoffeeScript):

  • CoffeeScript requires less punctuation, and relies even more on indentation:
  • Variables have different scope: every variable in CoffeeScript is as if it was declared nonlocal in Python. This makes it easier to access variables in enclosing scopes, but also easier to accidentally re-use variables.
  • The typing systems differ: CoffeeScript uses prototype object orientation, while Python uses multiple-inheritance object orientation. The main practical difference is that multiple inheritance is not supported by CoffeeScript. In principle, it's also easier to create "classes" in CoffeeScript because every object can act as a class.
  • lambda-style inline functions can be multiple lines in CoffeeScript, making mixed imperative/functional programming even easier. On the other hand, CoffeeScript functions do not support keyword arguments.
  • CoffeeScript's REPL has a different interface for multiline inputs: press CTRL-V before the first newline, and press CTRL-V again when done (instead of a blank line).
  • The built-in types differ in many small ways, e.g., their method names differ. But for the most part, there is a one-to-one mapping.
  • CoffeeScript has more helpful syntax for a lot of important features, but also misses a few features:
    • There is just one Number type corresponding to Python's float. There are no (big) integers, complex numbers, or rationals.
    • String interpolation, regular expressions, and the equivalent of range have built-in syntax (instead of relying on methods/libraries/functions).
    • There are two slicing operators depending on whether you want to include the final index or not.
    • All comparisons are shallow; no built-in deep comparison support. Truthiness is similarly shallow; e.g., [] is considered true.
    • unless alternative to if not; switch alternative to long if/then/else chains; and if can come at the end of a line;
    • Multiline ifs, while, and for loops are expressions instead of statements, so single statements span multiple lines with full indentation support. (while and for loops helpfully accumulate the list of final values.)
    • Three types of for loops, including cleaner syntax for Python's for i, x in enumerate(...) and for key, value in d.items().
    • Exceptional behavior generally doesn't raise exceptions like it does in Python. For example, d[key] returns undefined when key not in d (instead of raising KeyError); 1/0 returns Infinity (instead of raising ZeroDivisionError); and function calls with incorrect number of arguments work fine (with missing arguments set to undefined and extra arguments discarded, instead of raising TypeError).
    • No dictionary comprehensions or generator expressions.
    • No operator overloading via __special_methods__. No metaclasses.

Given the large similarites, a natural question is whether it's possible to automatically convert Python to CoffeeScript. We've started exploring that question in python2coffee. By contrast, this guide aims to teach you CoffeeScript so you can write code in CoffeeScript in the first place.

Quick Reference Guide

At a high level, if you remove all the colons at the end of lines from Python code, you end up with equivalent CoffeeScript code. There are many smaller differences, though, stemming in particular from different built-in types (a consequence of JavaScript). This section aims to document these differences by way of side-by-side examples.

print and assert

The analog of Python's print is CoffeeScript's console.log. Like Python 3, it is just a regular function. Similarly, assert's analog is console.assert (also a regular function, unlike Python).

PythonCoffeeScript
# Python 2
print "Hello world", 1+2
# Python 3
print("Hello world", 1+2)
#or
print ("Hello world", 1+2)
console.log "Hello world", 1+2
#or
console.log('Hello world', 1+2)
#INVALID: no space allowed between function and argument paren
#console.log ('Hello world', 1+2)
assert even % 2 == 0
assert even % 2 == 0, 'even numbers divisible by 2'
console.assert even % 2 == 0
console.assert even % 2 == 0, 'even numbers divisible by 2'

Comments

Python and CoffeeScript comments are generally the same:

PythonCoffeeScript
# This line is a comment
x = 5  # set x to five
# This line is a comment
x = 5  # set x to five

CoffeeScript also supports block comments (similar to triple-quoted strings in Python), which you should be careful not to trigger by accident:

PythonCoffeeScript
# Some comments
# Some more comments
# Even more comments
###
Some comments
Some more comments
Even more comments
###
### This line is a comment
## This line is a comment

Strings

CoffeeScript string notion is very similar syntax to Python, except in how triple-quoted strings deal with indentation. In addition, strings enclosed with "..." or """...""" have built-in string interpolation similar to Python 3.6's f-strings. (If you want something like Python's % operator, try the sprintf-js package.)

The resulting JavaScript String type has many similar methods to Python str, though often with different names.

PythonCoffeeScript
"Hello {}, your age is {}".format(name, age)
'Hello {}, your age is {}'.format(name, age)
"Hello {}, your age is {}".format(name,
  thisYear - birthYear)
#or
f'Hello {name}, your age is {thisYear - birthYear}'
"Hello #{name}, your age is #{age}"
#INVALID: 'Hello #{name}, your age is #{age}'
#  #{...} is allowed only in ""s, not ''s
"Hello #{name}, your age is #{thisYear - birthYear}"
s = '''\
hello
world'''
# 'hello\nworld'

s = '''
hello
world
'''
# '\nhello\nworld\n'
s = '''
  hello
  world
'''
# 'hello\nworld' -- common indentation removed
s = '''

  hello
  world

'''
# '\nhello\nworld\n'
'\033'   # 3-digit octal
'\x1b'   # 2-digit hex
'\u001b' # 4-digit hex
# octal forbidden in CoffeeScript
'\x1b'   # 2-digit hex
'\u001b' # 4-digit hex
'\a' # bell
'\b' # backspace
'\f' # formfeed
'\n' # linefeed
'\r' # carriage return
'\t' # tab
'\v' # vertical tab
'\x07' # bell
'\b' # backspace
'\f' # formfeed
'\n' # linefeed
'\r' # carriage return
'\t' # tab
'\v' # vertical tab
'y' in s
'y' not in s
s.startswith('y')
s.endswith('?')
s.find('hi')
s.find('hi', start)
s.rfind('hi')
s.rfind('hi', start)
s.find('hi') >= 0
s.replace('hi', 'bye')
s.replace('hi', 'bye', 1)
s.lower()
s.upper()
s.strip()
s.lstrip()
s.rstrip()
s.split()
s.split(',')
a = s.split(',', 2)
', '.join(array)
s.count('hi')
'y' in s
'y' not in s
s.startsWith('y')
s.endsWith('?')
s.indexOf 'hi' # also supports RegExp
s.indexOf 'hi', start
s.lastIndexOf 'hi' # also supports RegExp
s.lastIndexOf 'hi', start
s.includes 'hi'
s.replace /hi/g, 'bye'
s.replace 'hi', 'bye'
s.toLowerCase()
s.toUpperCase()
s.trim()      # no argument allowed
s.trimStart() # no argument allowed
s.trimEnd()   # no argument allowed
s.split /\s+/
s.split ','
a = s.split ','; a[2..] = [a[2..].join ',']
array.join(', ')
(s.match /hi/g).length
s1 + s2 + s3
s1 + s2 + s3
#or
s1.concat s2, s3
s * 5
s.repeat 5
len(s)
s.length
ord(s)
chr(27)
s.charCodeAt()
String.fromCharCode 27
isinstance(s, str)
typeof s == 'string'
str(x)
x.toString()

See also string slicing.

Numbers

JavaScript has just one Number type corresponding to Python's float (IEEE double precision). There are no built-in integers, big integers, complex numbers, or rationals. (But BigInts are coming!)

PythonCoffeeScript
6.0001
7.0
7e9
6.0001
7
7e9
0b11111111
0o377
255
0xff
0b11111111
0o377
255
0xff
int('ff', 16)
parseInt 'ff', 16
float('7e9')
parseFloat '7e9'
str(n)
bin(n)
oct(n)
hex(n)
n.toString()
n.toString(2)
n.toString(8)
n.toString(16)
str(n)
bin(n)
oct(n)
hex(n)
n.toString()
n.toString(2)
n.toString(8)
n.toString(16)
isinstance(n, (int, float))
typeof s == 'number'
(-b + (b**2 - 4*a*c)**0.5) / (2*a)
(-b + (b**2 - 4*a*c)**0.5) / (2*a)
x // y # integer division
x % y  # mod in [0, y)
x // y # integer division
x %% y # mod in [0, y)
(~a | b) & (c ^ d) << n
(~a | b) & (c ^ d) << n
math.e
math.pi
math.tau
math.inf
math.nan
Math.E
Math.PI
2*Math.PI
Infinity
NaN
round(x)
math.trunc(x)
math.floor(x)
math.ceil(x)
math.sqrt(x)
abs(x)
math.log(x)
math.log(x, base)
math.log1p(x)
math.log2(x)
math.log10(x)
Math.exp(x)
Math.expm1(x)
math.degrees(x)
math.radians(x)
math.cos(x)
math.sin(x)
math.tan(x)
math.acos(x)
math.asin(x)
math.atan(x)
math.atan2(y, x)
math.hypot(x, y)
Math.round x
Math.trunc x
Math.floor x
Math.ceil x
Math.sqrt x
Math.abs x
Math.log x
(Math.log x) / Math.log base
Math.log1p x
Math.log2 x
Math.log10 x
Math.exp x
Math.expm1 x
x * 180 / Math.PI
x * Math.PI / 180
Math.cos x
Math.sin x
Math.tan x
Math.acos x
Math.asin x
Math.atan x
Math.atan2 y, x
Math.hypot x, y # or more args

Functions

CoffeeScript functions automatically return the last expression (if they are not aborted with an explicit return), making the final return optional. All arguments default to undefined unless otherwise specified. Defaults are evaluated during the function call (unlike Python which evalutes them at function definition time).

PythonCoffeeScript
def rectangle(x, y = None):
  if y is None:
    y = x
  return x * y
rectangle = (x, y = x) -> x * y
def f(x, add = 1):
  y = x + add
  return y*y
f = (x, add = 1) ->
  y = x + add
  y*y
add1 = lambda x: x+1
add = lambda x, y=1: x+y
zero = lambda: 0
add1 = (x) -> x+1
add = (x, y=1) -> x+y
zero = -> 0
def callback(x):
  print('x is', x)
  return f(x)
compute('go', callback)
compute 'go', (x) ->
  console.log 'x is', x
  f(x)

In CoffeeScript (like Perl and Ruby), the parentheses in function calls are allowed but optional; when omitted, they implicitly extend to the end of the line. (Similar behavior is possible in IPython via the %autocall magic.) Parentheses are still required for zero-argument function calls and when an argument list ends before the end of the line. In CoffeeScript (unlike Python), there can be no space between the function name and the parentheses.

PythonCoffeeScript
f(5)
#or
f (5)

f(5, 10)
#or
f (5, 10)
f(5)
#or
f 5
# f (5) is technically valid but only by luck
f(5, 10)
#or
f 5, 10
# f (5, 10) is INVALID: no space allowed between function and argument paren
add(5, add1(add1(zero())))
add 5, add1 add1 zero()
add(add1(1), 2)
add add1(1), 2
#or
add (add1 1), 2

CoffeeScript functions do not support keyword arguments. The typical workaround is to use an object for an argument.

PythonCoffeeScript
f(add = 2, x = 10)
f(10, 2)
# no keyword arguments in function calls
def g(x, **options):
  f(x, **options)
g = (x, options) ->
  f(x, options.add)

CoffeeScript allows calling a function with a variable number of arguments, and getting back all remaining arguments, with splats (...):

PythonCoffeeScript
x = [1, 2, 3]
print(*x)
#or
apply(print, x)
x = [1, 2, 3]
console.log ...x
#or
console.log.apply console, x
def add(first, *rest):
  for arg in rest:
    first += rest
  return first
add = (first, ...rest) ->
  for arg in rest
    first += arg
  first

Instead of ...x, you can also write x.... If you're using CoffeeScript v1, you need to use the latter form.

Variable scoping

CoffeeScript has no variable declarations like Python's global and nonlocal. CoffeeScript's only behavior is the equivalent of Python's nonlocal: a variable is local to a function if it is assigned in that function and it is not assigned in any higher scope. An exception is that function arguments are always local to the function, with together with CoffeeScript's do makes it simple to explicitly request Python's default behavior.

PythonCoffeeScript
def f(x):
  def next():
    nonlocal x
    x += 1
    return x
  return [next(), next()]
f = (x) ->
  next = ->
    x += 1  # implicitly return x
  [next(), next()]
def f(x):
  def g(x):
    return -x
  return g(x+1)
f = (x) ->
  g = (x) -> -x
  g x+1
def delay(bits):
  out = []
  for bit in bits:
    def g(bit = bit): # save current bit
      return bit
    out.append(g)
  return out
delay = (bits) ->
  for bit in bits
    do (bit) -> # force locally scoped bit
      -> bit
def recurse(x, stack = []):
  for item in x:
    stack.append(item)
    recurse(item)
    stack.pop()
recurse(root)
stack = []
recurse = (x) ->
  for item in x
    stack.push item
    recurse item
    stack.pop()
recurse root

if/then/else and switch

CoffeeScript ifs are similar to Python's, except that elif is spelled else if. In addition, CoffeeScript offers unless as a more intuitive if not, and allows a one-line suffixed if (and unless).

PythonCoffeeScript
if x:
  y = 1
else:
  y = 2
if x
  y = 1
else
  y = 2
if error:
  return
if error
  return
#or
return if error
if not ok:
  continue
unless ok
  continue
#or
continue unless ok
y = 1 if x else 2
y = if x then 1 else 2
#or
y =
  if x
    1
  else
    2
if x:
  y = 1
elif z:
  y = 2
else:
  y = 3
if x
  y = 1
else if z
  y = 2
else
  y = 3
y = 1 if x else (2 if z else 3)
y =
  if x
    1
  else if z
    2
  else
    3

Unlike Python, CoffeeScript offers a switch expression as an alternative to if/then/else. switch is especially concise when branching on the same value. There's also a one-line form for switch cases, when...then.

PythonCoffeeScript
if x:
  y = 1
elif z:
  y = 2
else:
  y = 3
switch
  when x
    y = 1
  when z
    y = 2
  else
    y = 3
y = 1 if x else (2 if z else 3)
y =
  switch
    when x
      1
    when z
      2
    else
      3
if x == 0:
  print('zero')
elif x == 1 or x == 2 or x == 3:
  print('small')
elif x in ['hello', 'world']:
  print('hi')
else:
  print('unknown')
switch x
  when 0
    console.log 'zero'
  when 1, 2, 3
    console.log 'small'
  when 'hello', 'world'
    console.log 'hi'
  else
    console.log 'unknown'
def string(x):
  if x == 0:
    return 'zero'
  elif x == 1 or x == 2 or x == 3:
    return 'small'
  elif x in ['hello', 'world']:
    return 'hi'
  else:
    return 'unknown'
string = (x) ->
  switch x
    when 0 then 'zero'
    when 1, 2, 3 then 'small'
    when 'hello', 'world' then 'hi'
    else 'unknown'

while loops

CoffeeScript while loops are roughly identical to Python's, including support for continue and break. Like unless, until is shorthand for while not. In addition, loop is shorthand for while true. Like if and unless, while and until have a one-line suffix form. In addition, while and until loops are expressions, returning an array of the final values.

PythonCoffeeScript
while this or that:
  ...
while this or that
  ...
while items:
  items.pop()
items.pop() while items.length
reversed = []
while items:
  reversed.append(items.pop())
reversed =
  while items.length
    items.pop()
while not done:
  ...
until done
  ...
while True:
  ...
  if done:
    break
loop
  ...
  break if done
line = getNextLine()
while line:
  if not line.strip():
    continue
  ...
  line = getNextLine()
while (line = getNextLine())
  continue unless line.trim()
  ...

for loops

CoffeeScript for...in loops are similar to Python's, including support for continue and break, plus a concise notation for for...in enumerate(...). In addition, for loops are expressions (like while and until loops), returning an array of the final values.

PythonCoffeeScript
for x in [1, 2, 3]:
  y += x
for x in [1, 2, 3]
  y += x
for i, x in enumerate([1, 2, 3]):
  y += x * i
for x, i in [1, 2, 3]
  y += x * i
out = []
for x in [1, 2, 3]:
  out.append(x * x)
#or
out = [x * x for x in [1, 2, 3]]
out =
  for x in [1, 2, 3]
    x * x
#or
out = (x * x for x in [1, 2, 3])
for x in range(10):
  y += x
for x in [0...10]
  y += x
for x in range(5, 10):
  y += x
for x in [5...10]
  y += x
for x in range(5, 10, 2):
  y += x
for x in [5...10] by 2
  y += x

See also comprehensions.

Comprehensions

CoffeeScript array comprehensions are similar to Python's list comprehensions, but written with parentheses instead of brackets and with when instead of if. Unlike Python, they are just a one-line inverted form of a regular for loop (symmetric to the one-line inverted if), and can also be written in the non-inverted multiline form.

PythonCoffeeScript
y = [f(i) for i in x]
#or
y = list(map(f, x))
y = (f i for i in x)
#or
y =
  for i in x
    f i
#or
y = x.map f
y = [f(i) for i in x if condition(i)]
#or
y = list(filter(condition, map(f, x)))
y = (f i for i in x when condition i)
#or
y =
  for i in x when condition i
    f(i)
#or
y =
  for i in x
    continue unless condition i
    f(i)
z = [[f(i,j) for j in y] for i in x]
y = (f i, j for j in y for i in x)
#or
y = ((f i, j for j in y) for i in x)
#or
y =
  for i in x
    for j in y
      f i, j
z = [f(i,j) for i in x for j in y]
y = [].concat ...(f i, j for j in y for i in x)
#or
y = [].concat ...(
  for i in x
    for j in y
      f i, j
)

CoffeeScript lacks dictionary/object comprehensions and generator expressions, though it does have generator functions, which can be used to simulate generator expressions:

PythonCoffeeScript
it = (f(i) for i in x)
for item in it:
  ...
it = do -> yield f(i) for i in x
for item from it
  ...

Comparison operators

Most Python comparison/Boolean operators have the same name in CoffeeScript (in addition to offering C-style names). CoffeeScript also supports chained comparisons just like Python. One key difference is that == and != are shallow comparisons, not deep: they act like Python's ==/!= only for numbers and strings, and act like Python's is/isnt for all other objects. Another important difference is that [] and {} are considered true in CoffeeScript, so you need to check nonemptyness differently.

PythonCoffeeScript
True
False
true
false
1+2 == 3  # True
1 < 2 < 3 # True
1+2 == 3  # true
1 < 2 < 3 # true
x == 5 and not (y < 5 or done)
x == 5 and not (y < 5 or done)
b = bool(object)
b = Boolean object
#or
b = not not object
#or
b = !!object
if items: # check for nonempty list
  process(items)
if items.length # check for nonempty list
  process items
x = [1, 2, 3]
y = [1, 2, 3]
# pointer comparison
x is x    # True
x is y    # False
# deep comparison
x == x    # True
x == y    # True
x = [1, 2, 3]
y = [1, 2, 3]
# pointer comparison
x == x    # true
x == y    # false
# deep comparison
_ = require 'underscore' #or lodash
_.isEqual x, x  # true
_.isEqual x, y  # true

Python list / CoffeeScript Array

CoffeeScript arrays include notation similar to Python lists, as well as an indentation-based notion that avoids the need for commas.

The resulting JavaScript Array type has many similar methods to Python list, though often with different names.

PythonCoffeeScript
x = [1, 2, 3]
x = [1, 2, 3]
#or
x = [
  1
  2
  3
]
3 in x
3 not in x
3 in x
3 not in x
len(x)
x.length
x = []
x.append(5)
x.append(10)
x = []
x.push 5, 10
x = [1, 2, 3]
y = [4, 5, 6]
x.extend(y)
x = [1, 2, 3]
y = [4, 5, 6]
x.push ...y
x + y + z
x.concat y, z
#or
[...x, ...y, ...z]
last = x.pop()
first = x.pop(0)
last = x.pop()
first = x.shift()
x.reverse()
x.reverse()
# Lexical sort by string value
# e.g. [1, 10, 2]
x.sort(key = lambda item: str(item))
# Lexical sort by string value
# e.g. [1, 10, 2]
x.sort()
# Sort by numeric value
# e.g. [1, 2, 10]
x.sort(key = lambda item: float(item))
# Sort by numeric value
# e.g. [1, 2, 10]
x.sort (x,y) -> x-y
# or, especially for stable sort:
_ = require 'underscore' #or lodash
x = _.sortBy x
min(x)
max(x)
min(a, b)
max(a, b)
Math.min ...x
Math.max ...x
Math.min a, b
Math.max a, b
try:
  i = x.index(a)
except ValueError:
  i = -1
i = x.indexOf a
try:
  i = x.index(a, 5)
except ValueError:
  i = -1
i = x.indexOf a, 5

CoffeeScript destructuring assignment is a nice tool for extracting parts of arrays:

PythonCoffeeScript
a, b = b, a
[a, b] = [b, a]
head, *tail = [1, 2, 3]
[head, ...tail] = [1, 2, 3]

See also array slicing.

CoffeeScript has no analog to Python's tuple type, but the same effect of "an unchangable list" can be obtained via Object.freeze:

PythonCoffeeScript
x = (1, 2)
x = Object.freeze [1, 2]

Python dict / CoffeeScript Object

Python has two key/value mechanisms: hasattr/getattr/setattr for object attributes and __contains__/__getitem__/__setitem__ for dictionaries. CoffeeScript has no such asymmetry, making regular Objects a fine substitute for dictionaries.

PythonCoffeeScript
d = {1: 2, 'hello': 'world'}
d = {1: 2, hello: 'world'}
#or
d =
  1: 2
  hello: 'world'
d = dict((i, i**2) for i in range(10))
d = Object.fromEntries([i, i**2] for i in [0...10])
#or
d = Object.fromEntries(
  for i in [0...10]
    [i, i**2]
)
d.get(key)
d[key]
d.get('hello')
d.hello
#or
d['hello']
d.set('hello', 'bye')
d.hello = 'bye'
#or
d['hello'] = 'bye'
del d[key]
delete d[key]
key in d
key of d
for key in d:
  f(key)
for key of d
  f key
# Safer version, in case Object has added properties:
for own key of d
  f key
for key, value in d.items():
  ...
for key, value of d
  ...
list(d.keys())
list(d.values())
list(d.items())
Object.keys d
Object.values d
Object.entries d
len(d)
Object.keys(d).length
if d: # nonempty dict?
  process(items)
if Object.keys(d).length # nonempty Object?
  process d
d.setdefault(key, value)
d[key] ?= value
d.setdefault(key, []).append(value)
(d[key] ?= []).push value

CoffeeScript destructuring assignment is a nice tool for extracting parts of objects:

PythonCoffeeScript
a = d['a']
b = d['b']
c = d['c']
c_x = c['x']
{a, b, c: {x}} = d
head, *tail = [1, 2, 3]
[head, ...tail] = [1, 2, 3]

An important limitation of Objects as key/value stores is that all keys are mapped to strings: the reference d[x] is equivalent to d[x.toString()]. One weird consequence is that d[42] and d['42'] refer to the same item. On the plus side, it gives you a limited way to override key equality testing (like Python's __hash__ and __eq__), by using/overriding the toString method:

PythonCoffeeScript
d[1,2] = 'a'
d[[1,2]] = 'a'
# equivalent to d['1,2'] = 'a'
# d has keys of the form (int, int)
for x, y in d:
  ...
for (x, y), value in d.items():
  ...
# d has keys of the form 'int,int'
for key of d
  [x, y] = key.split ','
  ...
for key, value of d
  [x, y] = key.split ','
  ...
class Pair:
  def __init__(self, x, y):
    self.x = x
    self.y = y
  def __hash__(self):
    return hash((self.x, self.y))
  def __eq__(a, b):
    return a.x == b.x and a.y == b.y
d[Pair(1,2)] = 'a'
class Pair
  constructor: (@x, @y) ->
  toString: -> "#{@x},#{@y}"
d[new Pair 1, 2] = 'a'
# equivalent to d['1,2'] = 'a'

Python dict / CoffeeScript Map

While CoffeeScript Objects are a good substitute for dictionaries, they have a few limitations, most notably, that all keys in Objects get mapped to strings. An alternative is the built-in Map type, which allows arbitrary object keys, and distinguishes numbers (42) from their string equivalents ('42'). Unlike Python's dict, however, Maps offer no way to override the hash function or equality testing (Python's __hash__ and __eq__): objects are treated as identical keys if they match according to CoffeeScript's == operator, so numbers and strings are compared by value, while all other objects are compared according to reference identity (like Python's is). (Also, unlike ==, NaN is treated equal to itself and -0 and +0 are treated equal.) Map's syntax is also uglier than regular Objects.

PythonCoffeeScript
d = {1: 2, 'hello': 'world'}
d = new Map [
  [1, 2]
  ['hello', 'world']
]
len(d)
d.size
d.get(key)
d.get key
d[key] = value
d.set key, value
del d[key]
d.delete key
key in d
d.has key
for key, value in d.items():
  ...
for [key, value] from d
  ...
d.keys()
d.values()
d.items()
d.keys()
d.values()
d.entries()
d.setdefault(key, value)
d.set key, value unless d.has key

Python set / CoffeeScript Set

CoffeeScript offers a Set class similar to CoffeeScript's Map. It shares the limitation of testing equality according to CoffeeScript's == operator, so numbers and strings are compared by value, while all other objects are compared according to reference identity (like Python's is). If you instead want "render as identical strings" semantics, use regular CoffeeScript Objects (with constant values, e.g., true).

PythonCoffeeScript
x = set()
x = new Set
x = {1, 2, 3}
x = new Set [1, 2, 3]
# x is a set
5 in x
x.add(5)
x.discard(7)
# x is a set
x.has 5
x.add 5
x.delete 7
# x is a set
if x:
  print len(x), 'elements'
else:
  print 'empty'
# x is a Set
if x.size
  console.log x.size, 'elements'
else
  console.log 'empty'
# x is a set
for item in x:
  print item
# x is a Set
for item from x
  console.log item
iter(x)
x.values()

Slicing and range

CoffeeScript slicing features two notations for the range from i to j: i...j excludes j like Python's i:j, while i..j includes j.

PythonCoffeeScript
list(range(7, 10))
# [7, 8, 9]
[7...10]
#or
[7..9]
list(range(5))
# [0, 1, 2, 3, 4]
[0...5]
#or
[0..4]
# [...5] and [..4] are invalid
list(range(0, 10, 2))
# [0, 2, 4, 6, 8]
(i for i in [0...10] by 2)
# [0...10] by 2 is invalid
# list not generated (in Python 3)
for i in range(9999):
  ...
# list not generated
for i in [0...9999]
  ...
# x is str or list
x[7:10] # 7, 8, 9
# x is String or Array
x[7...10] # 7, 8, 9
#or
x[7..9]   # 7, 8, 9
# x is list
x[7:10] = ['a', 'b']
x.insert(0, 'c')
x.insert(7, 'd')
del x[7]
del x[7:10]
x.clear()
y = x.copy()
# x is Array
x[7...10] = ['a', 'b']
x.unshift 'c'
x[7...7] = ['d']
x[7...7] = []
x[7...10] = []
x[..] = []
y = x[..]
# x is str or list
x[:] # shallow copy
# x is String or Array
x[..] # shallow copy
# x is str or list
x[:-1]
# x is String or Array
x[...-1]

Note that negative numbers behave like Python in slices, but negative numbers behave differently when simply getting an item: x[-1] is equivalent to x['-1'] and will typically return undefined; to access the last element, use x[x.length-1].

Null values

Python has one "null" value, None. CoffeeScript has two, undefined and null. Essentially, undefined is the default initial value for all variables (a notion absent from Python), while null is an explicit null value.

PythonCoffeeScript
x = None
# x is automatically undefined
# explicit setting:
x = undefined
# alternate None-like value:
x = null

CoffeeScript defines an existential ? operator, both a unary form to test for undefined or null, and a binary form to provide alternate (default) values in case of undefined or null:

PythonCoffeeScript
if x is not None:
  ...
if x?
  ...
#equivalent to:
if x != undefined and x != null
  ...
y = 5 if x is None else x
y = x ? 5

CoffeeScript also defines many conditional operators that apply the operator only when the left-hand side isn't undefined or null (and otherwise leaves it alone):

PythonCoffeeScript
try:
  x
except UnboundLocalError:
  x = 5
x ?= 5
# d is a dictionary
d.setdefault(key, value)
# d is an object
d[key] ?= value
d[key] if d is not None else d
d?[key]
if callback is not None:
  callback(value)
callback?(value)
#or
callback? value
if x is not None and hasattr(x, 'set') and x.set is not None:
  x.set(5)
x?.set?(5)
#or
x?.set? 5

Regular expressions

CoffeeScript has built-in Perl-like /.../ syntax for regular expressions, and a triple-slash version ///.../// for multiline regular expressions that ignores whitespace.

PythonCoffeeScript
r = re.compile(r'^Hello (\w+)',
      re.IGNORECASE | re.MULTILINE)
r = /^Hello (\w+)/im
r = re.compile(r'<p>.*?</p>', re.DOTALL)
# ECMAScript 2018 / Node 9+
r = /<p>.*?<\/p>/s
# Older
r = /<p>[^]*?<\/p>/
r = re.compile(r'[(\[]*(\d+)/(\d+)/(\d+)[)\]]*')
r = /[(\[]*(\d+)\/(\d+)\/(\d+)[)\]]*/
#or
r = ///
  [(\[]*        # leading brackets
  (\d+) / (\d+) / (\d+)  # y/m/d
  [)\]]*        # closing brackets
///
def bracket(word):
  return re.compile(r'^[(\[]*' + word + r'[)\]]*')
bracket = (word) ->
  new RegExp "^[(\\[]*#{word}[)\\]]*"
#or
bracket = (word) ->
  /// ^[(\\[]* #{word} [)\\]]* ///
match = r.search(string)
match.group(0) # whole match
match.group(1) # first group
match.start()  # start index
match.string   # input string
match = r.exec string
match[0]     # whole match
match[1]     # first group
match.index  # start index
match.input  # input string
if r.search(string):
  ...
if r.test string
  ...
for match in re.finditer(r'(pattern)', string):
  match.group(0) # whole match
  match.group(1) # first group
  match.start()  # start index
  match.string   # input string
r = /(pattern)/g  # keeps lastIndex
while (match = r.exec string)?
  match[0]     # whole match
  match[1]     # first group
  match.index  # start index
  match.input  # input string
# r.exec can be re-used on other strings
matches = re.findall(r'pattern', string)
matches = string.match /pattern/g
# Array of full matches, ignoring all groups
out = re.sub(r'pattern', repl, string)
out = string.replace /pattern/g, repl
out = re.sub(r'pattern', repl, string, 1)
out = string.replace /pattern/, repl
out = re.sub(r'(pattern)', r'$(\1) \g<0>', string)
out = string.replace /(pattern)/g, '$$($1) $&'
def replacer(match):
  all = match.group(0)
  group1 = match.group(1)
  index = match.start()
  string = match.string
  ...
out = re.sub(r'(pattern)', replacer, string)
out = string.replace /(pattern)/g,
  (all, group1, index, string) ->
    # (unneeded arguments can be omitted)
    ...
out = re.split(r'\s*,\s*', string)
out = string.split /\s*,\s*/
out = re.split(r'\s*(,)\s*', string)
out = string.split /\s*(,)\s*/
out = re.split(r'\s*,\s*', string)[:limit]
out = string.split /\s*,\s*/, limit

JavaScript regular expression syntax and usage is roughly the same as Python, with some exceptions:

  • JavaScript doesn't support (?P<...>...), (?<=...), (?<!...), (?#...), (?(...)...|...), \A, or \Z in regular expressions.
  • In JavaScript, / needs to be escaped as \/ within /.../. Also, spaces right next to the surrounding /s can confuse the parser, so you may need to write them as [ ] (for example).
  • JavaScript doesn't support flag-changing syntax: instead of (?i) and (?m), use /.../i and /.../m.
  • JavaScript doesn't support flags re.ASCII, re.DEBUG, or re.LOCALE. (///.../// is the analog of re.VERBOSE.)
  • JavaScript's \d matches just [0-9], and \w matches just [a-zA-Z_], instead of the Unicode notions matched by Python. However, \s matches all Unicode space characters like Python.
  • JavaScript replacement patterns need to use $... instead of \... (and $50 instead of \g<50>), and thus need to have $ escaped as $$. The full-match replacer \g<0> should instead be $&. Additional replacement features are $`, which expands to the portion of the string before the match, and $', which expands to the portion of the string after the match.

Classes

CoffeeScript classes behave similar to Python classes, but are internally implemented with prototypes, so do not support multiple inheritence. CoffeeScript provides @ as a helpful alias for this (the analog of self in Python), and @foo as an alias for @.foo.

PythonCoffeeScript
class Point:
  def __init__(self, x, y):
    self.x = x
    self.y = y
  def translate(self, dx, dy):
    self.x += dx
    self.y += dy
  def __str__(self):
    return "({}, {})".format(self.x, self.y)
class Point
  constructor: (@x, @y) ->
  translate: (dx, dy) ->
    @x += dx
    @y += dy
    null  # otherwise, would return @y
  toString: ->
    "(#{@x}, #{@y})"
p = Point(1, 2)
p.translate(3, 4)
print(p)
p = new Point 1, 2
p.translate 3, 4
console.log "#{p}"
#or
console.log p.toString()
# Note: console.log p will not call toString()
trans = p.translate
trans(5, 6)
trans = p.translate
# trans 5, 6 will use this = global scope
trans.call p, 5, 6
#or
trans = p.translate.bind p
trans 5, 6
#or
trans = (...args) -> p.translate ...args
trans 5, 6
isinstance(p, Point) # True
isinstance(p, object) # True in Python 3
p instanceof Point # true
p instanceof Object # true
class PPoint(Point):
  dim = 2
  @classmethod
  def parse(Class, string):
    return Class(*[float(word)
      for word in string.split(",")])
class PPoint extends Point
  @dim: 2
  @parse: (string) ->
    # @ = this is the class in an @method
    new @ ...(parseFloat word \
      for word in string.split ",")
print(PPoint.dim)
PPoint.slope = lambda self: self.y / self.x
console.log PPoint::dim
PPoint::slope = -> @y / @x

-> vs. =>

Within methods, use => in place of -> to construct a function with the same value of this (@) as the method creating it.

PythonCoffeeScript
class Accumulator:
  def __init__(self):
    self.value = 0
  def adder(self):
    def add(x):
      self.value += x
    return add
class Accumulator
  constructor: ->
    @value = 0
  adder: ->
    (x) => @value += x

Exceptions

Exceptions are far less important in CoffeeScript than Python because most invalid operations return special values like NaN or undefined instead of raising an exception.

PythonCoffeeScript
try:
  x = a/b
except ZeroDivisionError:
  if a > 0:
    x = math.inf
  elif a < 0:
    x = -math.inf
  else:
    x = math.nan
x = a/b
try:
  x = d[key]
except KeyError:
  x = None
#or
x = d.get(key)
x = d[key]
# f either needs 1 argument
# or needs 0 arguments
try:
  f(argument)
except TypeError:
  f()
# f will ignore excess arguments
f argument
# f either needs 1 argument
# or needs 0 arguments
try:
  f()
except TypeError:
  f(None)
# Missing f argument defaults to undefined
f()

But exceptions still exist, and can be captured in a similar way: CoffeeScript try is similar to Python's, except there is no exception type matching.

PythonCoffeeScript
try:
  f()
except Exception as e:
  ...
finally:
  cleanup()
try
  f()
catch e
  ...
finally
  cleanup()
try:
  f()
except ErrorType1 as e:
  ...
except ErrorType2 as e:
  ...
except Exception as e:
  ...
try
  f()
catch e
  if e instanceof ErrorType1
    ...
  else if e instanceof ErrorType1
    ...
  else
    ...
raise RuntimeError('Oops!')
throw new Error 'Oops!'

See JavaScript's built-in Error types.

Generator functions

CoffeeScript generator functions are roughly identical to Python's: any yield statement makes a function a generator function, and both support yield from. The main difference is in the resulting Generator object, which has a different next() interface and does not support send(). Most important is that looping over generators (and iterators in general) requires for...from instead of for...in in CoffeeScript.

PythonCoffeeScript
def positive_ints():
  n = 0
  while True:
    n += 1
    yield n
positive_ints = ->
  n = 0
  loop
    n += 1
    yield n
for i in positive_ints():
  ...
for i from positive_ints()
  ...
def concat(iter1, iter2):
  yield from iter1
  yield from iter2
concat = (iter1, iter2) ->
  yield from iter1
  yield from iter2
L = [1, 2]
it = iter(L)
one = it.next()
two = it.next()
L = [1, 2]
it = L[Symbol.iterator]()
one = it.next().value
two = it.next().value
try:
  it.next()
  done = False
except StopIteration:
  done = True
done = it.next().done

Asynchronous functions

CoffeeScript async functions are similar to Python's (which coevolved to be similar to JavaScript's), except that there's no need to declare a function async; like generator functions, any function with an await keyword is automatically async.

PythonCoffeeScript
async def process(db):
  data = await db.read('query')
  return f(data)
process = (db) ->
  data = await db.read 'query'
  f data
async def fast():
  return 'done'
fast = ->
  await 'done'
async def slow():
  print('hello')
  await asyncio.sleep(1)
  print('world')
sleep = (seconds) ->
  new Promise (resolve) ->
    setTimeout resolve, seconds * 1000
slow = ->
  console.log 'hello'
  await sleep 1
  console.log 'world'

Installation / Getting Started

To install CoffeeScript on your machine, first install NodeJS. (LTS = Long Term Support is a good choice.) Or install NodeJS with a package manager. This will install (at least) two commands, node and npm.

Then run the following command:

npm install --global coffeescript

You should then have a command coffee that runs the interactive interpreter, similar to python. The main difference is, if you want to input a multiline command, you need to press CTRL-V before the first newline, and press CTRL-V again when you're done (instead of indicating completion with a blank line).

You can also compile a CoffeeScript file filename.coffee into a JavaScript file filename.js via

coffee -c filename.coffee

Modules (on Node)

Like Python, you can split up your code into multiple files, and add dependencies between them. In a Node environment, the analog of import is require:

PythonCoffeeScript
import helper # load ./helper.py
helper = require './helper' # load ./helper.coffee or ./helper.js
#or
helper = require './helper.coffee'
from helper import data, scheme
{data, scheme} = require './helper'

A key difference is that modules need to explicitly "export" variables that they want to expose to other modules by attaching them to the exports object; local variables are private.

PythonCoffeeScript
# helper.py
reps = 5
data = 'stuff' * reps
scheme = lambda: 'http'
del reps # hide variable from outside
# helper.coffee
reps = 5
exports.data = 'stuff'.repeat reps
exports.scheme = -> 'http'

Here is how to detect whether you are the "main" module (being executed directly by coffee or node):

PythonCoffeeScript
if __name__ == '__main__':
  ...
if require.main == module
  ...

Packages (on Node)

The analog of PyPI (Python Package Index) is NPM (Node Package Manager). The analog of command-line tool pip is npm (or an alternative called yarn).

Unlike PyPI, NPM packages are usually installed locally to each project, which makes it easy for different projects to use different versions of the same package. To get started, run

npm init

This will ask questions for the creation of a stub package.json file.

Then you can install packages local to your project using npm install. For example, to install the underscore package (written by the same author as CoffeeScript), run

npm install underscore

This will install the package in node_packages/underscore, and change package.json to note which version you depend on.

You can use a package installed in this way via

_ = require 'underscore'

It's also easy to create your own packages and publish them for others to use.

About

This document is by Erik Demaine, with helpful input from several others. The top image is based on the CoffeeScript logo and this free Python clipart.

About

CoffeeScript for Python programmers (a guide)

Resources

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published