PONY λ M2 Modula-2

Python.CodeCompared.To/Julia

An interactive executable cheatsheet comparing Python and Julia

Python 3.13 Julia 1.12
Output & Running
Hello, World
print("Hello, World!")
println("Hello, World!")
Julia's println() is the equivalent of Python's print(). There is also print() (no trailing newline). Like Python, a top-level statement is a complete program — no main or class needed.
Interpolated output
name = "Alice" age = 30 print(f"{name} is {age}")
name = "Alice" age = 30 println("$name is $age")
Julia interpolates with $name for a bare variable and $(expr) for an expression — the counterpart to Python f-strings, but with no f prefix and parentheses instead of braces.
Comments
# a single-line comment """ A docstring / block comment. """ print("done")
# a single-line comment #= A multi-line block comment. =# println("done")
Julia shares Python's # single-line comment but adds a real block-comment syntax, #= ... =#, which Python lacks.
Variables & Types
Declaring variables
message = "hi" count = 3 count = count + 1 print(message, count)
message = "hi" count = 3 count = count + 1 println(message, " ", count)
Variable assignment looks the same — no declaration keyword and dynamic typing. Note that Julia's println does not insert spaces between arguments, so you add separators yourself.
Constants
MAX_RETRIES = 3 print(MAX_RETRIES)
const MAX_RETRIES = 3 println(MAX_RETRIES)
Unlike Python's convention-only constants, Julia has a real const keyword. It chiefly fixes the variable's type for performance; reassigning a new value of the same type only warns.
Type annotations
value: int = 42 print(value) print(type(value).__name__)
value::Int = 42 println(value) println(typeof(value))
Python type hints are optional documentation ignored at runtime. Julia's ::Int annotations are real: the compiler uses them for performance and, crucially, for dispatch. typeof() returns the concrete type.
None vs. nothing
result = None print(result is None)
result = nothing println(result === nothing)
Julia's equivalent of None is the singleton nothing, tested with ===. Julia also has a distinct missing for absent data (as in statistics), a separation Python's single None does not make.
Multiple assignment & swapping
first, second = 1, 2 first, second = second, first print(first, second)
first, second = 1, 2 first, second = second, first println(first, " ", second)
Tuple assignment and the swap idiom a, b = b, a work identically in Julia.
Strings
Common string operations
text = "Hello World" print(text.upper()) print(text.lower()) print(len(text))
text = "Hello World" println(uppercase(text)) println(lowercase(text)) println(length(text))
Julia exposes string operations as free functions — uppercase(text), length(text) — rather than methods on the string object as in Python. This reflects Julia's function-first, not object-first, design.
Concatenation & repetition
greeting = "ab" + "cd" line = "-" * 5 print(greeting, line)
greeting = "ab" * "cd" line = "-" ^ 5 println(greeting, " ", line)
A surprise for Python developers: Julia concatenates strings with * (not +) and repeats them with ^. The * choice reflects that concatenation is not commutative, like matrix multiplication.
Slicing & indexing
word = "javascript" print(word[0:4]) print(word[-6:])
word = "javascript" println(word[1:4]) println(word[end-5:end])
Julia is 1-based and its ranges are inclusive, so the first four characters are word[1:4]. The keyword end refers to the last index, replacing Python's negative indices.
Split & join
csv = "a,b,c" parts = csv.split(",") print(parts) print("-".join(parts))
csv = "a,b,c" parts = split(csv, ",") println(parts) println(join(parts, "-"))
Julia's split and join are free functions taking the string first. Note join(parts, "-") takes the separator as the second argument, unlike Python's separator-first "-".join(parts).
Replacing substrings
text = "Hello World" print(text.replace("World", "Julia"))
text = "Hello World" println(replace(text, "World" => "Julia"))
Julia's replace takes a pair ("old" => "new") rather than two separate arguments. The => pair syntax appears throughout Julia, including dictionary construction.
Numbers
Arithmetic & division
print(7 + 2) print(7 / 2) print(7 // 2) print(7 % 2)
println(7 + 2) println(7 / 2) println(div(7, 2)) println(7 % 2)
Like Python, Julia's / always produces a float (3.5). Integer (floor) division is div(7, 2) or the ÷ operator, where Python uses //.
Exponentiation
print(2 ** 10)
println(2 ^ 10)
Julia spells exponentiation with a single caret ^, not Python's **. (In Julia ** is not an operator at all.)
Integer overflow & big integers
big = 1_000_000_000 print(big * big) print(2 ** 100)
value = 1_000_000_000 println(value * value) println(big(2) ^ 100)
A sharp difference: Python integers are unbounded automatically, but Julia's default Int is a fixed 64-bit machine integer that silently overflows. For arbitrary precision you opt in explicitly with big(2)^100.
Parsing & math
import math print(int("42")) print(math.sqrt(16)) print(max(3, 7))
println(parse(Int, "42")) println(sqrt(16)) println(max(3, 7))
Julia parses with parse(Int, "42"), and math functions such as sqrt live in Base — no import math needed, since Julia's base library is large and always available.
Arrays & Broadcasting
Creating & indexing (1-based!)
fruits = ["apple", "banana", "cherry"] print(fruits[0]) print(fruits[-1]) print(len(fruits))
fruits = ["apple", "banana", "cherry"] println(fruits[1]) println(fruits[end]) println(length(fruits))
The single biggest adjustment: Julia arrays are 1-based, so the first element is fruits[1] and the last is fruits[end]. Length comes from length().
Adding & removing
numbers = [1, 2, 3] numbers.append(4) numbers.pop() print(numbers)
numbers = [1, 2, 3] push!(numbers, 4) pop!(numbers) println(numbers)
Julia uses push! and pop!. By convention, a trailing ! marks functions that mutate their argument — a naming discipline Python does not have.
Comprehensions
numbers = [1, 2, 3, 4] doubled = [number * 2 for number in numbers] evens = [number for number in range(10) if number % 2 == 0] print(doubled) print(evens)
numbers = [1, 2, 3, 4] doubled = [number * 2 for number in numbers] evens = [number for number in 0:9 if number % 2 == 0] println(doubled) println(evens)
Array comprehensions are nearly identical to Python's, including the if filter clause. The main difference is the range: Julia's 0:9 is inclusive of both ends, where Python's range(10) stops before 10.
Sum, sort & reduce
numbers = [3, 1, 4, 1, 5] print(sum(numbers)) print(sorted(numbers)) words = ["banana", "apple"] print(sorted(words, key=len))
numbers = [3, 1, 4, 1, 5] println(sum(numbers)) println(sort(numbers)) words = ["banana", "apple"] println(sort(words, by=length))
Julia's sum and sort mirror Python's built-ins, but the sort customizer is named by= rather than key=. The in-place variant is sort!.
Splatting & destructuring
first, *rest = [1, 2, 3, 4] print(first, rest) combined = [*[1, 2], *[3, 4]] print(combined)
first, rest... = [1, 2, 3, 4] println(first, " ", rest) combined = [[1, 2]; [3, 4]] println(combined)
Julia's slurping operator is a trailing ... (rest...), placed after the name rather than before it as in Python's *rest. Vertical concatenation [a; b] joins arrays.
Broadcasting with the dot
numbers = [1, 2, 3, 4] doubled = [n * 2 for n in numbers] plus_ten = [n + 10 for n in numbers] print(doubled) print(plus_ten)
numbers = [1, 2, 3, 4] doubled = numbers .* 2 plus_ten = numbers .+ 10 println(doubled) println(plus_ten)
Julia's signature feature: the dot turns any operator into an element-wise (broadcast) operation, so numbers .* 2 multiplies every element with no loop or comprehension. This is the idiomatic, high-performance way Python developers would reach to NumPy for.
Broadcasting a function
import math values = [1.0, 4.0, 9.0] roots = [math.sqrt(value) for value in values] print(roots)
values = [1.0, 4.0, 9.0] roots = sqrt.(values) println(roots)
Appending a dot to a function name (sqrt.(values)) applies it element-wise across an array. Any function — including ones you define — can be broadcast this way, which is more general than Python's comprehension or NumPy's pre-vectorized functions.
Lazy generators
squares = (n * n for n in range(5)) print(next(squares)) print(next(squares))
squares = (n * n for n in 0:4) state = iterate(squares) value, next_state = state println(value) value2, _ = iterate(squares, next_state) println(value2)
Both languages have lazy generator expressions with the same (expr for x in ...) syntax. Julia advances an iterator with the iterate function (returning a value/state pair) rather than Python's next().
Dicts
Creating & accessing
person = {"name": "Alice", "age": 30} print(person["name"]) print(person.get("email", "n/a"))
person = Dict("name" => "Alice", "age" => 30) println(person["name"]) println(get(person, "email", "n/a"))
A Julia Dict is built with => pairs and accessed with brackets like a Python dict. The fallback lookup is get(dict, key, default), mirroring Python's dict.get.
Adding & updating
scores = {} scores["math"] = 95 scores["math"] += 1 print(scores)
scores = Dict{String, Int}() scores["math"] = 95 scores["math"] += 1 println(scores)
Assignment and += on keys work as in Python. An empty typed dict is written Dict{String, Int}(); you can also write Dict() for an untyped one.
Iterating entries
person = {"age": 30, "score": 95} for key, value in person.items(): print(key, value)
person = Dict("age" => 30, "score" => 95) for (key, value) in person println(key, " ", value) end
Iterating a Julia Dict yields key/value pairs directly — no .items() needed. Note that a Julia Dict is unordered, like Python dicts before 3.7, so do not rely on insertion order.
Checking for a key
config = {"debug": True} print("debug" in config) print("verbose" in config)
config = Dict("debug" => true) println(haskey(config, "debug")) println(haskey(config, "verbose"))
Julia tests for a key with haskey(dict, key). Its booleans are lowercase true/false, unlike Python's capitalized True/False.
Keys & values
counts = {"a": 1, "b": 2} print(sorted(counts.keys())) print(sorted(counts.values()))
counts = Dict("a" => 1, "b" => 2) println(sort(collect(keys(counts)))) println(sort(collect(values(counts))))
Julia's keys() and values() return lazy iterators; wrap them in collect() to get a concrete array, much as you would wrap Python's views in list().
Control Flow
if / elseif / else
score = 75 if score >= 90: print("A") elif score >= 70: print("B") else: print("C")
score = 75 if score >= 90 println("A") elseif score >= 70 println("B") else println("C") end
Julia uses no colons and closes every block with end rather than relying on indentation. The keyword is elseif (one word), close to Python's elif.
Conditional expression
age = 20 status = "adult" if age >= 18 else "minor" print(status)
age = 20 status = age >= 18 ? "adult" : "minor" println(status)
Julia uses the C-style ternary condition ? a : b (the spaces around ? and : are required), rather than Python's a if condition else b.
Conditions require a Bool
items = [] if not items: print("empty")
items = [] if isempty(items) println("empty") end
Julia has no truthiness: an if condition must be an actual Bool, and if [] is an error. You test emptiness explicitly with isempty(), where Python relies on an empty list being falsy.
Boolean operators
ready = True done = False print(ready and not done) print(ready or done)
ready = true done = false println(ready && !done) println(ready || done)
Julia uses the symbolic operators &&, ||, and !, in contrast to Python's English keywords and, or, and not.
Equality & identity
print(1 == 1.0) print("a" == "a") print([1, 2] == [1, 2])
println(1 == 1.0) println("a" == "a") println([1, 2] == [1, 2])
Julia's == compares by value (so 1 == 1.0 is true), just like Python. Julia's === tests identity/egality, comparable to Python's is.
Loops & Iteration
Counting loop
for index in range(3): print(index)
for index in 1:3 println(index) end
Julia loops over a range literal 1:3, which is inclusive of both ends — it yields 1, 2, 3. This differs twice over from Python's range(3), which is 0-based and stops before 3.
Iterating a collection
for color in ["red", "green"]: print(color)
for color in ["red", "green"] println(color) end
Iterating a collection reads the same as Python's for x in collection, just with end closing the block instead of dedentation.
Index and value together
for index, color in enumerate(["red", "green"]): print(index, color)
for (index, color) in enumerate(["red", "green"]) println(index, " ", color) end
Julia also has enumerate(), but its indices start at 1, not 0, consistent with Julia's 1-based arrays.
while, break & continue
count = 0 while True: count += 1 if count == 3: continue if count >= 5: break print(count)
count = 0 while true global count += 1 if count == 3 continue end if count >= 5 break end println(count) end
break and continue behave as in Python. One catch: at the top level (outside a function), assigning to an outer variable from inside a loop needs the global keyword, a scoping rule Python does not impose.
Functions
Defining functions
def greet(name): return f"Hi, {name}" print(greet("Alice"))
function greet(name) return "Hi, $name" end println(greet("Alice"))
Julia uses function ... end instead of def and indentation. Julia also returns the value of the last expression implicitly, so the return keyword is often optional.
One-line functions
square = lambda value: value * value print(square(5))
square(value) = value * value println(square(5))
Julia's assignment-form function definition, square(value) = value * value, is a concise full function — more capable than Python's single-expression lambda. Anonymous functions use value -> value * value.
Default arguments
def greet(name, greeting="Hello"): return f"{greeting}, {name}" print(greet("Bob")) print(greet("Bob", "Hi"))
function greet(name, greeting="Hello") return "$greeting, $name" end println(greet("Bob")) println(greet("Bob", "Hi"))
Default positional arguments work just as in Python. Julia evaluates the default fresh on each call, so it has no mutable-default-argument pitfall.
Keyword arguments
def make_user(name, *, role="user", active=True): return (name, role, active) print(make_user("Alice", role="admin"))
function make_user(name; role="user", active=true) return (name, role, active) end println(make_user("Alice", role="admin"))
Julia separates keyword arguments from positional ones with a semicolon in the signature (name; role="user"). Callers then pass them by name, much like Python's keyword-only arguments after a bare *.
Variadic arguments
def total(*numbers): return sum(numbers) print(total(1, 2, 3, 4))
function total(numbers...) return sum(numbers) end println(total(1, 2, 3, 4))
Julia collects extra positional arguments with a trailing ... after the parameter name (numbers...), the counterpart to Python's leading-star *numbers.
Returning multiple values
def min_max(numbers): return min(numbers), max(numbers) low, high = min_max([3, 1, 4, 1, 5]) print(low, high)
function min_max(numbers) return minimum(numbers), maximum(numbers) end low, high = min_max([3, 1, 4, 1, 5]) println(low, " ", high)
Julia returns a tuple and destructures it on assignment, exactly as Python does. Note the function names are minimum/maximum for collections; min/max compare their arguments.
Closures
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter(), counter())
function make_counter() count = 0 function increment() count += 1 return count end return increment end counter = make_counter() println(counter(), " ", counter())
Julia closures capture and can reassign an enclosing local variable without any keyword, so there is no need for Python's nonlocal declaration.
Functions as arguments
def apply_twice(func, value): return func(func(value)) print(apply_twice(lambda x: x + 3, 10))
function apply_twice(func, value) return func(func(value)) end println(apply_twice(x -> x + 3, 10))
Functions are first-class values in Julia just as in Python; an anonymous function is written x -> x + 3. Julia idiom often places such a function as the first argument (for example to map or filter), enabling do-block syntax.
Types & Multiple Dispatch
Structs vs. classes
class Point: def __init__(self, x, y): self.x = x self.y = y point = Point(1, 2) print(point.x, point.y)
struct Point x::Int y::Int end point = Point(1, 2) println(point.x, " ", point.y)
A Julia struct declares typed fields and gets a constructor automatically — no __init__ and no self. Crucially, structs hold data only; behaviour lives in functions defined outside the type.
Mutable structs
class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 counter = Counter() counter.increment() counter.increment() print(counter.count)
mutable struct Counter count::Int end function increment!(counter::Counter) counter.count += 1 end counter = Counter(0) increment!(counter) increment!(counter) println(counter.count)
Julia structs are immutable by default; you opt into mutation with mutable struct. Methods are not defined inside the type — increment! is a free function that takes the struct as an argument.
Multiple dispatch
class Circle: def __init__(self, radius): self.radius = radius def area(self): return 3.14159 * self.radius ** 2 class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height print(round(Circle(2).area(), 2)) print(Rectangle(3, 4).area())
struct Circle radius::Float64 end struct Rectangle width::Float64 height::Float64 end area(shape::Circle) = 3.14159 * shape.radius ^ 2 area(shape::Rectangle) = shape.width * shape.height println(round(area(Circle(2.0)), digits=2)) println(area(Rectangle(3.0, 4.0)))
This is Julia's defining idea: instead of a method living inside a class, you define multiple area methods that the compiler selects by argument type. Dispatch considers all argument types, not just the receiver as Python's self.area() does.
Abstract types & hierarchy
class Animal: def speak(self): return "..." class Dog(Animal): def speak(self): return "woof" def describe(animal): return animal.speak() print(describe(Dog()))
abstract type Animal end struct Dog <: Animal end speak(animal::Dog) = "woof" describe(animal::Animal) = speak(animal) println(describe(Dog()))
Julia builds hierarchies from abstract types, with <: meaning "is a subtype of". Concrete types like Dog hold data and cannot be subtyped further — composition and dispatch replace Python's class inheritance.
Parametric types
# Python uses typing generics for hints only from typing import TypeVar T = TypeVar("T") def first_element(items: list[T]) -> T: return items[0] print(first_element([10, 20, 30]))
function first_element(items::Vector{T}) where {T} return items[1] end println(first_element([10, 20, 30]))
Julia's parametric types (Vector{T} where T) are checked and used by the compiler for specialization, unlike Python's TypeVar generics, which are documentation that the runtime ignores.
Error Handling
try / except vs. try / catch
try: result = 10 // 0 except ZeroDivisionError as error: print("caught:", error)
try result = div(10, 0) catch error println("caught: ", error) end
Julia uses catch (binding the error after the keyword, no as) and closes with end. A single catch handles all exception types; you branch on the type inside with isa if needed.
Raising errors
def withdraw(amount): if amount < 0: raise ValueError("amount must be positive") return amount try: withdraw(-5) except ValueError as error: print(error)
function withdraw(amount) if amount < 0 throw(ArgumentError("amount must be positive")) end return amount end try withdraw(-5) catch error println(error.msg) end
Julia raises with throw and provides built-in exception types such as ArgumentError and BoundsError. The message is reached via the exception's .msg field.
finally
try: print("working") finally: print("cleanup")
try println("working") finally println("cleanup") end
The finally block runs regardless of whether an exception occurred, identically to Python.
Custom exceptions
class ValidationError(Exception): pass try: raise ValidationError("invalid input") except ValidationError as error: print("validation:", error)
struct ValidationError <: Exception message::String end try throw(ValidationError("invalid input")) catch error println("validation: ", error.message) end
A custom Julia exception is a struct that subtypes Exception — there are no exception base-class methods to inherit, just a plain type carrying whatever fields you give it.
Modules & Imports
Base needs no import
import math print(math.gcd(12, 8)) print(math.factorial(5))
println(gcd(12, 8)) println(factorial(5))
Many functions Python places in modules like math are part of Julia's always-available Base, so gcd and factorial need no import at all.
Using a standard library
from statistics import mean print(mean([1, 2, 3, 4]))
using Statistics println(mean([1, 2, 3, 4]))
Julia's using Statistics brings a whole module's exported names into scope, comparable to Python's from statistics import * but the standard idiom. Use import instead when you want to qualify names.
Importing across files
# greetings.py def hello(name): return f"Hi, {name}" # main.py from greetings import hello print(hello("Alice"))
# Greetings.jl module Greetings export hello hello(name) = "Hi, $name" end # main.jl using .Greetings println(hello("Alice"))
Julia groups code into a module with explicit exports, then pulls names in with using. These examples span multiple definitions/files, so they cannot run in this single-file runner.