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.