Common Mistakes

This page contains a short summary of common mistakes (โš ๏ธ), opinionated style suggestions (๐Ÿงน) and miscellaneous tips (๐Ÿ’ก). Keep in mind that all rules are meant to be broken in the right context!

Table of Contents

Dependencies

โš ๏ธ Remove unused dependencies

Dependencies like Test.jl and Revise.jl don't belong in the main Project.toml.

Refer to Environments and Enhancing the REPL respectively.

๐Ÿงน Use explicit imports

Explicitly import only the functions you need from your dependencies.

# โŒ BAD:
using LinearAlgebra # import all of LinearAlgebra just to use `cholesky` and `det`

# โœ… GOOD:
using LinearAlgebra: cholesky, det # explicitly import only `cholesky` and `det`

Refer to Writing a Julia Package: Organizing dependencies, source files and exports. Advanced developers can test for this using ExplicitImports.jl.

๐Ÿงน Keep imports and exports in main file

Keep imports and exports in one place instead of spreading them over several files.

# โœ… GOOD:
module MyPackage

# 1.) Explicitly import the functions you need from your dependencies
using LinearAlgebra: cholesky, det

# 2.) Include source files
include("timestwo.jl")
include("timesthree.jl")

# 3.) Export functions you defined
export timestwo, timesthree

end # end module

Refer to Writing a Julia Package: Organizing dependencies, source files and exports.

๐Ÿงน You probably don't need submodules

Julia programmers tend to not use submodules for individual source files. Submodules are usually only necessary if your code requires isolated namespaces (e.g. if you need multiple unexported functions of the same name).

Functions

โš ๏ธ Avoid overly strict type annotations

Restrictive types prohibit Julia's composability. For example, a function that only allows Float64 won't be compatible with ForwardDiff.jl's Dual number type. If needed, use generic types like Number, Real or Integer.

# โŒ Restrictive:
timestwo(x::Float32) = 2 * x

# โœ… Flexible:
timestwo(x) = 2 * x

For arrays, use AbstractArray, AbstractMatrix, AbstractVector. To prohibit array types that don't use 1-based indexing, call Base.require_one_based_indexing within your function.

# โŒ Restrictive:
sumrows(A::Matrix{Float64}) = sum(eachrow(A))

# A bit more flexible:
sumrows(A::Matrix{<:Real}) = sum(eachrow(A))

# โœ… Flexible:
sumrows(A::AbstractMatrix) = sum(eachrow(A))

# or lightly restrict the element-type while keeping array-type flexible:
sumrows(A::AbstractMatrix{<:Real}) = sum(eachrow(A))

โš ๏ธ Avoid accidental type promotions

Floating point numbers like 1.0 are of type Float64. Multiplying an array of Float32 by such a number will promote the output to an array of Float64.

The functions typeof, eltype, one(T), zero(T) and convert(T, x) function are your friends.

# โŒ BAD:
function example_bad(A::AbstractArray)
    return 1 / sqrt(2) * A
end

# โœ… GOOD:
function example_good(A::AbstractArray)
    T = eltype(A)
    scale = convert(T, 1 / sqrt(2))
    return scale * A
end

โš ๏ธ Don't assume 1-based indexing

We can't assume all arrays to use 1-based indexing (see e.g. OffsetArrays.jl). Use iterators like eachrow, eachcol, eachslice and eachindex when possible. Otherwise, call Base.require_one_based_indexing.

Refer to the section Iterating over arrays in Lecture 2: Arrays & Linear Algebra.

โš ๏ธ Avoid type instabilities

Type instabilities are discussed in Profiling: Type stability and should be avoided, as they have a strong negative effect on performance. However, type stability is not mandatory for the project work in this course.

For advanced developers (besides profiling), type instabilities can be uncovered using JET.jl.

โš ๏ธ Avoid output type annotations

If your code is type stable (see previous point), Julia will be able to infer output types without annotations. Output type annotations can hurt performance by causing allocations via unwanted type conversions.

# โŒ BAD: return type annotation is abstract
sumrows(A::AbstractMatrix)::AbstractVector = sum(eachrow(A))

# โŒ BAD: return type annotation is too specific.
# This will likely cause an unwanted and slow type conversion.
sumrows(A::AbstractMatrix)::Vector{Float32} = sum(eachrow(A))

# โœ… GOOD: Just let Julia figure it out
sumrows(A::AbstractMatrix) = sum(eachrow(A))

๐Ÿงน Avoid strings for configuration

In Python, it is common to configure functions via string arguments. A 1-to-1 translation of this design pattern might look as follows:

# โŒ BAD:
function solve_bad(data; algorithm="default", kwargs...)
    if algorithm == "default"
        solve_default(data; kwargs...)
    elseif algorithm == "special"
        solve_special(data; kwargs...)
    else
        error("Unknown algorithm $algorithm")
    end
end

In Julia, it is more idiomatic to introduce types and use multiple dispatch:

# โœ… GOOD:
abstract type AbstractSolver end

# Solver without arguments:
struct DefaultSolver <: AbstractSolver end

# Solver with arguments:
struct SpecialSolver <: AbstractSolver
    some_parameter::Int
    another_parameter::Bool
end

solve(data) = solve(data, DefaultSolver())
solve(data, algorithm::DefaultSolver) = solve_default(data)
solve(data, algorithm::SpecialSolver) = solve_special(data, algorithm) # pass arguments via `algorithm`

# We could add this, but Julia will already throw a MethodError: 
# solve(data, algorithm) = error("Unknown algorithm $algorithm")

(If for some reason, you want to avoid introducing types, at least use symbols (:default, :special) instead of strings ("default", "special").)

๐Ÿ’ก Loops are perfectly fine

In NumPy and MATLAB, code is commonly vectorized. This is done to internally run for-loops in C/C++ code instead of the much slower Python and MATLAB. In Julia, for-loops are highly performant and don't need to be avoided โ€“ both loops and vectorization can be used.

Refer to the lists of noteworthy differences from Python and MATLAB.

๐Ÿ’ก Allocations are slow

Allocating memory for a new array is slow. Instead of allocating new arrays, we can often update values of existing arrays via mutation. By convention, Julia programmers indicate such functions with an ! at the end of the name (see e.g. sort vs. sort!).

Use Profiling to identify performance critical allocations. Then refer to the section on Views in Lecture 2: Arrays & Linear Algebra for mutation.

๐Ÿ’ก Leverage the type system

Julia's type system is quite powerful. Type parameters can not only be used in structs, but also in methods:

# Method where both inputs have to have the same type:
issametype(a::T, b::T) where {T} = true
issametype(a, b) = false

# Method where array element type is made accessible:
myeltype(A::AbstractArray{T}) where {T} = T

Types

โš ๏ธ Avoid overly strict struct fields, use type parameters

There is rarely a reason to restrict field types to something more concrete than Number, Real or Integer. However, these abstract types are bad for performance when used directly in fields (refer to section Performance in Lecture 4: Custom Types).

Type parameters are the perfect solution to both of these issues!

# โŒ Restrictive:
struct MyComplexRestrictive
    re::Float32
    im::Float32
end

# โŒ Slow:
struct MyComplexSlow
    re::Real
    im::Real
end

# โœ… Flexible & fast:
struct MyComplex{T<:Real}
    re::T
    im::T
end

Similar ideas can also be applied to fields containing arrays:

# โŒ Restrictive:
struct MyStructRestrictive
    mat::Matrix{Float32}
end

# โœ… More flexible.
# Note: All three structs put different restrictions on element and array types.
struct MyStruct1{T<:Real}
    mat::Matrix{T}
end

struct MyStruct2{A<:AbstractMatrix}
    mat::A
end

struct MyStruct3{T<:Real,A<:AbstractMatrix{T}}
    mat::A
end

โš ๏ธ Avoid mutable structs

Mutable struct are less performant than regular (non-mutable) structs, since they are generally allocated on the heap. It is therefore often more performant to simply return a new struct.

# โŒ USUALLY BAD:
mutable struct PointMutable{T<:Real}
    x::T
    y::T
end
 
# โŒ Mutate field of point:
function addx_bad!(pt::PointMutable, x) 
    pt.x += x
    return pt
end

# โœ… GOOD:
struct Point{T<:Real}
    x::T
    y::T
end

# โœ… Simply create new immutable point
addx(pt::Point, x) = Point(pt.x + x, pt.y)

Refer to the section on Mutable types in Lecture 4: Custom Types.

โš ๏ธ Avoid structs with untyped fields

Julia can't infer types from structs with untyped fields, which will result in bad performance. Use type parameters instead.

# โŒ BAD:
struct MyTypeBad
    x
end

# โœ… GOOD:
struct MyType{T}
    x::T
end
# โŒ BAD:
struct AnotherTypeBad{X}
    x::X
    y
end

# โœ… GOOD:
struct AnotherType{X,Y}
    x::X
    y::Y
end

Refer to the section Performance in Lecture 4: Custom Types.

Documentation

โš ๏ธ Adhere to docstring conventions

The Julia community has conventions in place when writing documentation. LLM tend to ignore these. Advanced developers can automate some of this work using DocStringExtensions.jl.

๐Ÿ’ก Run code

You can use Documenter.jl's @example blocks to run Julia code examples and show the outputs within your documentation. Refer to Writing documentation.

๐Ÿ’ก Run MLDatasets.jl code

To use MLDatasets.jl within your documentation, you need to set an environment flag in .github/workflows/CI.yaml:

docs:
  name: Documentation
  [...]

    - uses: julia-actions/julia-docdeploy@v1
      env:
        DATADEPS_ALWAYS_ACCEPT: true # <-- add this line for MLDatasets download
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}
Last modified: July 03, 2025.
Website, code and notebooks are under MIT License © Adrian Hill.
Built with Franklin.jl, Pluto.jl and the Julia programming language.