Gleam for Python users
Hello productive pragmatic Pythonistas!
Hello productive pragmatic Pythonistas!
In Python, comments are written with a #
prefix.
# Hello, Joe!
A docstring that occurs as the first statement in a module, function, class, or method definition will become the __doc__
attribute of that object.
def a_function():
"""Return some important data."""
pass
In Gleam, comments are written with a //
prefix.
// Hello, Joe!
Comments starting with ///
are used to document the following statement. Comments starting with ////
are used to document the current module.
//// This module is very important.
/// The answer to life, the universe, and everything.
const answer: Int = 42
You can reassign variables in both languages.
size = 50
size = size + 100
size = 1
Python has no specific variable keyword. You choose a name and that’s it!
Gleam has the let
keyword before its variable names.
let size = 50
let size = size + 100
let size = 1
Python supports basic, one directional destructuring (also called unpacking). Tuple of values can be unpacked and inner values can be assigned to left-hand variable names.
(a, b) = (1, 2)
# a == 1
# b == 2
# works also for for-loops
for key, value in enumerate(a_dict):
print(key, value)
In Gleam, let
and =
can be used for pattern matching, but you’ll get compile errors if there’s a type mismatch, and a runtime error if there’s a value mismatch. For assertions, the equivalent let assert
keyword is preferred.
let #(x, _) = #(1, 2)
let assert [] = [1] // runtime error
let assert [y] = "Hello" // compile error, type mismatch
Python is a dynamically typed language. Types are only checked at runtime and a variable can have different types in its lifetime.
Type hints are optional annotations that document the code with type information.
These annotations are accessible at runtime via the __annotations__
module-level variable.
These hints will mainly be used to inform static analysis tools like IDEs, linters…
some_list: list[int] = [1, 2, 3]
In Gleam type annotations can optionally be given when binding variables.
let some_list: List(Int) = [1, 2, 3]
Gleam will check the type annotation to ensure that it matches the type of the assigned value. It does not need annotations to type check your code, but you may find it useful to annotate variables to hint to the compiler that you want a specific type to be inferred.
In Python, you can define functions with the def
keyword. In that case, you have to use the return
keyword to return a value other than None
.
def sum(x, y):
return x + y
Anonymous functions returning a single expression can also be defined with the lambda
keyword and be assigned into variables.
mul = lambda x, y: x * y
mul(1, 2)
Gleam’s functions are declared using a syntax similar to Rust or JavaScript. Gleam’s anonymous functions have a similar syntax and don’t need a .
when called.
pub fn sum(x, y) {
x + y
}
let mul = fn(x, y) { x * y }
mul(1, 2)
In Python, top level functions are exported by default. Functions starting with _
are considered protected and should not be used outside of their defining scope.
In Gleam, functions are private by default and need the pub
keyword to be public.
// this is public
pub fn sum(x, y) {
x + y
}
// this is private
fn mul(x, y) {
x * y
}
Type hints can be used to optionally annotate function arguments and return types.
Discrepancies between type hints and actual values at runtime do not prevent interpretation of the code.
Static code analysers (IDE tooling, type checkers like mypy) are necessary to detect those errors.
def sum(x: int, y: int) -> int:
return x + y
def mul(x: int, y: int) -> bool:
# no errors from the interpreter.
return x * y
Functions can optionally have their argument and return types annotated in Gleam. These type annotations will always be checked by the compiler and throw a compilation error if not valid. The compiler will still type check your program using type inference if annotations are omitted.
pub fn add(x: Int, y: Int) -> Int {
x + y
}
pub fn mul(x: Int, y: Int) -> Bool { // compile error, type mismatch
x * y
}
As long as functions are in scope they can be assigned to a new variable. There is no special syntax to assign a module function to a variable.
Gleam has a single namespace for value and functions within a module, so there is no need for a special syntax to assign a module function to a variable.
fn identity(x) {
x
}
fn main() {
let func = identity
func(100)
}
Both Python and Gleam have ways to give arguments names and in any order.
Keyword arguments are evaluated once at function definition time, and there is no evidence showing a noticeable performance penalty when using named arguments.
When calling a function, arguments can be passed
def replace(inside: str, each: str, with_string: str):
pass
# equivalent calls
replace('hello world', 'world', 'you')
replace(each='world', inside='hello world', with_string='you')
In Gleam arguments can be given a label as well as an internal name. Contrary to Python, the name used at the call-site does not have to match the name used for the variable inside the function.
pub fn replace(inside string, each pattern, with replacement) {
go(string, pattern, replacement)
}
replace(each: ",", with: " ", inside: "A,B,C")
There is no performance cost to Gleam’s labelled arguments as they are optimised to regular function calls at compile time, and all the arguments are fully type checked.
Operator | Python | Gleam | Notes |
---|---|---|---|
Equal | == |
== |
In Gleam both values must be of the same type |
Strictly equal to | == |
== |
Comparison in Gleam is always strict. (see note for Python) |
Reference equality | is |
True only if the two objects have the same reference | |
Not equal | != |
!= |
In Gleam both values must be of the same type |
Greater than | > |
> |
In Gleam both values must be ints |
Greater than | > |
>. |
In Gleam both values must be floats |
Greater or equal | >= |
>= |
In Gleam both values must be ints |
Greater or equal | >= |
>=. |
In Gleam both values must be floats |
Less than | < |
< |
In Gleam both values must be ints |
Less than | < |
<. |
In Gleam both values must be floats |
Less or equal | <= |
<= |
In Gleam both values must be ints |
Less or equal | <= |
<=. |
In Gleam both values must be floats |
Boolean and | and |
&& |
In Gleam both values must be bools |
Logical and | and |
Not available in Gleam | |
Boolean or | or |
|| |
In Gleam both values must be bools |
Logical or | or |
Not available in Gleam | |
Add | + |
+ |
In Gleam both values must be ints |
Add | + |
+. |
In Gleam both values must be floats |
Subtract | - |
- |
In Gleam both values must be ints |
Subtract | - |
-. |
In Gleam both values must be floats |
Multiply | * |
* |
In Gleam both values must be ints |
Multiply | * |
*. |
In Gleam both values must be floats |
Divide | / |
/ |
In Gleam both values must be ints |
Divide | / |
/. |
In Gleam both values must be floats |
Remainder | % |
% |
In Gleam both values must be ints, in Gleam negative values behave differently: Use int.modulo to mimick Python’s behavior. |
Concatenate | + |
<> |
In Gleam both values must be strings |
Pipe | |> |
Gleam’s pipe can pipe into anonymous functions. This operator does not exist in python |
Some notes for Python:
==
is by default comparing by value:
0
and 1
that will be coerced to False
and True
respectively==
two objects with the same members values won’t be equal:
__eq__
operator is redefined.In Python, top-level declarations are in the global/module scope is the highest possible scope. Any variables and functions defined will be accessible from anywhere in the code.
There is no notion of constant variables in Python.
from typing import Final
# in the global scope
THE_ANSWER: Final = 42
In Gleam constants can be created using the const
keyword.
const the_answer = 42
pub fn main() {
the_answer
}
Python blocks are always associated with a function / conditional / class declarations… There is no way to create multi-line expressions blocks like in Gleam.
Blocks are declared via indentation.
def a_func():
# A block here
pass
In Gleam braces {
}
are used to group expressions.
pub fn main() {
let x = {
some_function(1)
2
}
let y = x * {x + 10} // braces are used to change arithmetic operations order
y
}
In Python, strings are stored as unicode code-points sequence. Strings can be encoded or decoded to/from a specific encoding.
In Gleam all strings are UTF-8 encoded binaries.
"Hellø, world!"
"Hellø, world!"
Tuples are very useful in Gleam as they’re the only collection data type that allows mixed types in the collection.
Python tuples are immutable, fixed-size lists that can contain mixed value types. Unpacking can be used to bind a name to a specific value of the tuple.
my_tuple = ("username", "password", 10)
_, password, _ = my_tuple
let my_tuple = #("username", "password", 10)
let #(_, password, _) = my_tuple
Lists in Python are allowed to have values of mixed types, but not in Gleam.
Python can emulate the cons
operator of Gleam using the *
operator and unpacking:
list = [2, 3, 4]
[head, *tail] = list
# head == 2
# tail == [3, 4]
Gleam has a ..
(known as cons
) operator that works for lists destructuring and pattern matching. In Gleam lists are immutable so adding and removing elements from the start of a list is highly efficient.
let list = [2, 3, 4]
let list = [1, ..list]
let [1, second_element, ..] = list
[1.0, ..list] // compile error, type mismatch
In Python, dictionaries can have keys of any type as long as:
hashable
, such as integers, strings, tuples (due to their immutable values), functions… and custom objects implementing the __hash__
method.In Gleam, dicts can have keys and values of any type, but all keys must be of the same type in a given dict and all values must be of the same type in a given dict.
There is no dict literal syntax in Gleam, and you cannot pattern match on a dict. Dicts are generally not used much in Gleam, as custom types are more common.
{"key1": "value1", "key2": "value2"}
{"key1": "1", "key2": 2}
import gleam/dict
dict.from_list([#("key1", "value1"), #("key2", "value2")])
dict.from_list([#("key1", "value1"), #("key2", 2)]) // Type error!
Case is one of the most used control flows in Gleam. It can be seen as a switch
statement on steroids. It provides a terse way to match a value type to an
expression. Gleam’s case
expression is fairly similar to Python’s match
statement.
Matching on primitive types:
def http_error(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
Matching on tuples with variable capturing:
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
Matching on type constructors:
match point:
case Point(x=0, y=0):
print("Origin is the point's location.")
case Point(x=0, y=y):
print(f"Y={y} and the point is on the y-axis.")
case Point(x=x, y=0):
print(f"X={x} and the point is on the x-axis.")
case Point():
print("The point is located somewhere else on the plane.")
case _:
print("Not a point")
The match expression supports guards, similar to Gleam:
match point:
case Point(x, y) if x == y:
print(f"The point is located on the diagonal Y=X at {x}.")
case Point(x, y):
print(f"Point is not on the diagonal.")
The case operator is a top level construct in Gleam:
case some_number {
0 -> "Zero"
1 -> "One"
2 -> "Two"
n -> "Some other number" // This matches anything
}
The case operator especially coupled with destructuring to provide native pattern matching:
case xs {
[] -> "This list is empty"
[a] -> "This list has 1 element"
[a, b] -> "This list has 2 elements"
_other -> "This list has more than 2 elements"
}
The case operator supports guards:
case xs {
[a, b, c] if a >. b && a <=. c -> "ok"
_other -> "ko"
}
and disjoint union matching:
case number {
2 | 4 | 6 | 8 -> "This is an even number"
1 | 3 | 5 | 7 -> "This is an odd number"
_ -> "I'm not sure"
}
Error management is approached differently in Python and Gleam.
Python uses the notion of exceptions to interrupt the current code flow and propagate the error to the caller.
An exception is raised using the keyword raise
.
def a_function_that_fails():
raise Exception("an error")
The callee block will be able to capture any exception raised in the block using a try/except
set of blocks:
try:
print("executed")
a_function_that_fails()
print("not_executed")
except Exception as e:
print("doing something with the exception", e)
In contrast in Gleam, errors are just containers with an associated value.
A common container to model an operation result is Result(ReturnType, ErrorType)
.
A Result is either:
Error(ErrorValue)
Ok(Data)
recordHandling errors actually means to match the return value against those two scenarios, using a case for instance:
case int.parse("123") {
Error(e) -> io.println("That wasn't an Int")
Ok(i) -> io.println("We parsed the Int")
}
In order to simplify this construct, we can use the use
expression with the
try
function from the gleam/result
module.
Ok(Something)
is matchedError(Something)
let a_number = "1"
let an_error = "ouch"
let another_number = "3"
use int_a_number <- try(parse_int(a_number))
use attempt_int <- try(parse_int(an_error)) // Error will be returned
use int_another_number <- try(parse_int(another_number)) // never gets executed
Ok(int_a_number + attempt_int + int_another_number) // never gets executed
Type aliases allow for easy referencing of arbitrary complex types. Even though their type systems does not serve the same function, both Python and Gleam provide this feature.
A simple variable can store the result of a compound set of types.
type Headers = list[tuple[str, str]]
# can now be used to annotate a variable
headers: Headers = [("Content-Type", "application/json")]
The type
keyword can be used to create aliases:
pub type Headers =
List(#(String, String))
let headers: Headers = [#("Content-Type", "application/json")]
Custom type allows you to define a collection data type with a fixed number of named fields, and the values in those fields can be of differing types.
Python uses classes to define user-defined, record-like types. Properties are defined as class members and initial values are generally set in the constructor.
By default the constructor does not provide base initializers in the constructor so some boilerplate is needed:
class Person:
name: str
age: int
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
person = Person(name="Jake", age=20)
# or with positional arguments Person("Jake", 20)
name = person.name
More recent alternatives use dataclasses
or leverage the
NamedTuple
base type to generate a constructor with initializers.
By default a class created with the dataclass
decorator is mutable (although
you can pass options to the dataclass
decorator to change the behavior):
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
person = Person(name="Jake", age=20)
name = person.name
person.name = "John" # The name is now "John"
NamedTuples
on the other hand are immutable:
from typing import NamedTuple
class Person(NamedTuple):
name: str
age: int
person = Person(name="Jake", age=20)
name = person.name
# cannot reassign a value
person.name = "John" # error
Gleam’s custom types can be used in much the same way that structs are used in Elixir. At runtime, they have a tuple representation and are compatible with Erlang records.
type Person {
Person(name: String, age: Int)
}
let person = Person(name: "Jake", age: 35)
let name = person.name
An important difference to note is there is no OOP in Gleam. Methods can not be added to types.
In Python unions can be declared with the |
operator.
In Gleam functions must always take and receive one type. To have a union of two different types they must be wrapped in a new custom type.
def int_or_float(x: int | float) -> str:
if isinstance(x, int):
return f"It's an integer: {x}"
else:
return f"It's a float: {x}"
type IntOrFloat {
AnInt(Int)
AFloat(Float)
}
fn int_or_float(x: IntOrFloat) {
case x {
AnInt(1) -> "It's an integer: 1"
AFloat(1.0) -> "It's a float: 1.0"
}
}
In Python, constructors cannot be marked as private. Opaque types can be imperfectly emulated using a class method and some magic property that only updates via the class factory method.
In Gleam, custom types can be defined as being opaque, which causes the constructors for the custom type not to be exported from the module. Without any constructors to import other modules can only interact with opaque types using the intended API.
from typing import NewType
# Is protected: people must not use it out side of this module!
_Identifier = NewType('Identifier', int)
def get_id() -> _Identifier:
return _Identifier(100)
pub opaque type Identifier {
Identifier(Int)
}
pub fn get_id() {
Identifier(100)
}
There is no special syntax to define modules as files are modules in Python
Gleam’s file is a module and named by the file name (and its directory path). Since there is no special syntax to create a module, there can be only one module in a file.
// in file wibble.gleam
pub fn identity(x) {
x
}
// in file main.gleam
import wibble // if wibble was in a folder called `lib` the import would be `lib/wibble`
pub fn main() {
wibble.identity(1)
}
# inside module src/nasa/moon_base.py
# imports module src/nasa/rocket_ship.py
from nasa import rocket_ship
def explore_space():
rocket_ship.launch()
Imports are relative to the root src
folder.
Modules in the same directory will need to reference the entire path from src
for the target module, even if the target module is in the same folder.
// inside module src/nasa/moon_base.gleam
// imports module src/nasa/rocket_ship.gleam
import nasa/rocket_ship
pub fn explore_space() {
rocket_ship.launch()
}
import unix.cat as kitty
import unix/cat as kitty
from animal.cat import Cat, stroke
def main():
kitty = Cat(name="Nubi")
stroke(kitty)
import animal/cat.{Cat, stroke}
pub fn main() {
let kitty = Cat(name: "Nubi")
stroke(kitty)
}