Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Julia  Module 4 – Tuples, dictionaries, strings and text

Dictionaries and sets, and the 1.13 order change

Build, query and update Julia dictionaries, see how a Dict compares its keys, write code that never relies on its order, and compare collections with sets.

  • Beginner
  • 20 minutes
  • Examples run with julia 1.13.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Build, query and update dictionaries with get, get!, haskey, delete! and mergewith
  • Explain how a Dict compares keys with isequal and hash
  • Write code and tests that never depend on the iteration order of a Dict
  • Compare two collections with sets and set operations

Before you start

What you’ll learn · Before you start

What you will learn

  • Build, query and update dictionaries with get, get!, haskey, delete! and mergewith
  • Explain how a Dict compares keys with isequal and hash
  • Write code and tests that never depend on the iteration order of a Dict
  • Compare two collections with sets and set operations

Before you start

On this page

Counting the letters of “mississippi” takes one line in a loop: counts[ch] = get(counts, ch, 0) + 1 gave 4 i’s, 1 m, 2 p’s and 4 s’s. Print a dictionary of five city names, though, and Julia 1.13.1 lists them as delhi, chennai, kolkata, mumbai, pune, while 1.12.7 and the 1.10 LTS list the same keys as kolkata, delhi, mumbai, chennai, pune, because Julia 1.13 replaced the hash function that decides where each key is stored.

This lesson builds, reads and updates dictionaries with runs recorded on all three releases. You will count and group with get and get!, see which keys a dictionary treats as the same, write code and tests that hold whatever the order, combine dictionaries with merge and mergewith, and compare two collections with sets.

A dictionary maps each key to one value

A dictionary: A collection of entries, each a key and a value, that finds the value of a key without searching the others: Julia's Dict. Each key appears once, and keys are compared with isequal., Julia’s Dict, holds entries of a key and a value and finds the value of a key without searching through the others. Each key appears once. The previous lesson built one from pairs; this program builds two by counting, the most common job a dictionary does:

word_counts.jl: counting letters and words, then grouping with get! Julia · word_counts.jl
# Counting with a dictionary: the letters of a word, then the words of a short weather note in Marathi and English.
counts = Dict{Char, Int}()
for ch in "mississippi"
    counts[ch] = get(counts, ch, 0) + 1          # get gives 0 while ch is not a key yet
end
println(sort!(collect(counts)))                  # sorted, so the print is the same on every run

note = """
आज पुण्यात पाऊस पडला. The rain started at six,
and the rain did not stop: पाऊस थांबला नाही.
आज शाळा बंद होती, so the gauge was read twice.
"""
words = split(lowercase(note), [' ', '\n', '.', ',', ':']; keepempty = false)
freq = Dict{String, Int}()
for w in words
    freq[w] = get(freq, w, 0) + 1
end
println(length(words), " words, ", length(freq), " different")
top = sort!(collect(freq); by = p -> (-p.second, p.first))   # most frequent first, then by word
for (w, n) in first(top, 5)
    println("  ", rpad(w, 6), n)
end
println("haskey(freq, \"rain\") = ", haskey(freq, "rain"), ", get(freq, \"snow\", 0) = ", get(freq, "snow", 0))
try
    freq["snow"]
catch err
    println(sprint(showerror, err))
end

by_station = Dict{String, Vector{Float64}}()
for (station, t) in [("Pune", 25.2), ("Kochi", 27.9), ("Pune", 24.8)]
    push!(get!(by_station, station, Float64[]), t)   # get! stores an empty vector the first time
end
println(sort!(collect(by_station)))
delete!(freq, "the")
println("after delete!: haskey = ", haskey(freq, "the"), ", ", length(freq), " keys")
word_counts.jl, run with Julia 1.13.1 Python · counts_session.py
from session import files, run

# Counting letters and words with get, then grouping with get!, on the current release.
files("word_counts.jl")
run("""
julia word_counts.jl
""")

Output

$ julia word_counts.jl
['i' => 4, 'm' => 1, 'p' => 2, 's' => 4]
28 words, 23 different
  the   3
  rain  2
  आज    2
  पाऊस   2
  and   1
haskey(freq, "rain") = true, get(freq, "snow", 0) = 0
KeyError: key "snow" not found
["Kochi" => [27.9], "Pune" => [25.2, 24.8]]
after delete!: haskey = false, 22 keys

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 counts_session.py

The first loop counts the letters. Step through it and watch what get returns for each one:

Interactive · step by step Counting the letters of "mississippi" with get Step through the loop. On the left is the letter the loop reads and what get returns for it; on the right, the entries of counts.
Counting the letters of "mississippi" with get, step 8 of 8. Showing ch ('i'), get(counts, ch, 0) (3: i is a key), 'm' =>, 'i' => (4), 's' => (4), 'p' => (2).ch'i'get(counts, ch, 0)3: i is a key'm' =>1'i' =>4's' =>4'p' =>2

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. An empty dictionary. counts = Dict{Char, Int}() starts empty: every key will be a Char and every value an Int.
  2. 'm' is new. The first letter is 'm'. It is not a key yet, so get returns the default, 0, and counts['m'] = 0 + 1 stores 1.
  3. 'i' and 's' are new. 'i' and then 's' are new too, so each gets its own entry, starting at 1.
  4. 's' again. The next 's' is a key now, so get returns its value, 1, and the line stores 2. The default is used only for a key that is missing.
  5. Four more letters. 'i', 's', 's' and 'i' each read their old count and add 1, so 'i' reaches 3 and 's' reaches 4.
  6. 'p', twice. 'p' is new and starts at 1; the second 'p' finds it and stores 2.
  7. The last letter. The last 'i' finds 3 and stores 4. The loop ran 11 times and the dictionary holds 4 keys, one for each letter that appeared.
  8. The counts. sort!(collect(counts)) gave ['i' => 4, 'm' => 1, 'p' => 2, 's' => 4], sorted by key so the print never depends on the order the dictionary keeps.

get(counts, ch, 0) returns the value stored for ch, or the default, 0, when ch is not a key yet, so one line both starts a new count and raises an old one. The same idiom counted the 28 words of a short weather note in Marathi and English, 23 of them different: “the” 3 times, and “rain”, “आज” and “पाऊस” twice each. The text was lowercased first, so “The” and “the” are one key.

Asking for a key that is missing is different from asking with a default. freq["snow"] stopped with KeyError: key "snow" not found, while haskey(freq, "rain") answered true and get(freq, "snow", 0) returned 0. Choose the form by what a missing key means: an error in your data, or a value you can supply.

get! goes one step further: when the key is missing it stores the default before returning it. push!(get!(by_station, station, Float64[]), t) created an empty vector for a station the first time it appeared and added to the same vector afterwards, which grouped the readings as "Pune" => [25.2, 24.8]. delete!(freq, "the") removed a key, leaving 22.

Printing a dictionary as it is

Wrong: println(freq) in a report or a test, which prints the entries in the order the dictionary keeps them. Right: sort!(collect(freq)), as every print in this lesson does, so the output is the same on every run and every release. The third section shows why.

Two keys are the same key when isequal says so

A dictionary needs a rule for “the same key”. The documentation gives it: keys are compared with isequal and hashed with hash. The two must agree: values that are isequal must have the same hash, or the dictionary could not find one by the other.

keys_isequal.jl: which keys are the same, and a key that changes Julia · keys_isequal.jl
# Which keys a Dict treats as the same: isequal decides, and hash must agree with it.
d = Dict(1 => :a, 1.0 => :b, -0.0 => :c, 0.0 => :d)
println(length(d), " entries; d[1] = ", repr(d[1]))
println(sort!(collect(d); by = string))          # sorted for a stable print
println("isequal(1, 1.0) = ", isequal(1, 1.0), ", hash(1) == hash(1.0): ", hash(1) == hash(1.0))
println("isequal(-0.0, 0.0) = ", isequal(-0.0, 0.0), ", although -0.0 == 0.0 is ", -0.0 == 0.0)

stations = Dict("Pune" => 1)
println("\"pune\" is a key: ", haskey(stations, "pune"), "; :Pune is a key: ", haskey(stations, :Pune))

key = [18.52, 73.86]                             # a vector as a key
places = Dict(key => "Pune")
push!(key, 560.0)                                # the key object changes after it was stored
println("length = ", length(places), ", haskey(places, key) = ", haskey(places, key))
println("haskey(places, [18.52, 73.86]) = ", haskey(places, [18.52, 73.86]))
fixed = Dict((18.52, 73.86) => "Pune")           # a tuple cannot change
println("tuple key found: ", haskey(fixed, (18.52, 73.86)))
keys_isequal.jl, run with Julia 1.13.1 Python · keys_session.py
from session import files, run

# Which keys a Dict treats as the same, and a key that changes after it was stored.
files("keys_isequal.jl")
run("""
julia keys_isequal.jl
""")

Output

$ julia keys_isequal.jl
3 entries; d[1] = :b
Pair{Real, Symbol}[-0.0 => :c, 0.0 => :d, 1.0 => :b]
isequal(1, 1.0) = true, hash(1) == hash(1.0): true
isequal(-0.0, 0.0) = false, although -0.0 == 0.0 is true
"pune" is a key: false; :Pune is a key: false
length = 1, haskey(places, key) = false
haskey(places, [18.52, 73.86]) = false
tuple key found: true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 keys_session.py

Four pairs gave three entries. isequal(1, 1.0) is true and hash(1) == hash(1.0) printed true, so 1 and 1.0 are one key, and d[1] returned :b, the value written with the key 1.0. -0.0 and 0.0 are two keys, because isequal(-0.0, 0.0) is false even though -0.0 == 0.0 is true, a difference the floating-point lesson met for NaN as well. Strings compare exactly: “pune” is not the key “Pune”, and the symbol :Pune is not the string either.

The last lines store a vector as a key and then change that vector. Step through the lookups that followed:

Interactive · message by message How a Dict finds a key, and how a changed key gets lost Step through three lookups. The Dict uses the key's hash to choose a slot, then isequal to compare the key stored there.
How a Dict finds a key, and how a changed key gets lost, step 6 of 6. Messages so far: Your code to The Dict: d[1]; The Dict to The slot: isequal(1, 1.0)?; The slot to Your code: :b; Your code to The Dict: haskey(places, key); The Dict to Your code: false; Your code to The slot: haskey(places, [18.52, 73.86]).Your codeThe Dicthash picks a slotThe slotkey [18.52, 73.86, 560.0]d[1]isequal(1, 1.0)?:bhaskey(places, key)falsehaskey(places, [18.52, 73.86])isequal: two numbers against three

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Ask for d[1]. d holds the entries 1.0 => :b, -0.0 => :c and 0.0 => :d. The code asks it for the key 1.
  2. The hash chooses a slot. hash(1) == hash(1.0) is true, so the Dict looks in the slot where the key 1.0 was stored.
  3. isequal says yes. isequal(1, 1.0) is true: for the Dict they are one key. That is why the four pairs gave 3 entries.
  4. A key that changed. places was built with the vector [18.52, 73.86] as its key. Then push!(key, 560.0) changed that vector, which the Dict still holds.
  5. The new hash. The changed vector has another hash, so the Dict looks in a slot where nothing was ever stored, and answers false.
  6. The old contents. A new [18.52, 73.86] has the old hash and leads to the right slot, but isequal finds three numbers there, not two: false again. The entry is still counted by length, and no lookup can reach it.

The entry was still there, length(places) was 1, but no lookup could reach it: the changed vector had a new hash value: The number hash(x) computes from a value, which a Dict or a Set uses to choose where to store it. Values that are isequal have the same hash; Julia 1.13 changed the hash of many values, hash(42) among them. that led to an empty slot, and a new vector with the old numbers led to the right slot but no longer compared equal. The documentation warns that changing a stored key can leave a hash table internally inconsistent. A tuple key, (18.52, 73.86), cannot change, and the program found it.

Key idea

A Dict finds a key by its hash and confirms it with isequal, so 1 and 1.0 are one key, -0.0 and 0.0 are two, and a key must never change after it is stored.

Checkpoint

Check: Reading and updating a Dict · How keys compare, with isequal and hash

  1. Question 1 of 2 With freq = Dict("rain" => 2), what does get(freq, "snow", 0) return, and what does freq hold afterwards?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0, and freq still holds only "rain" => 2

    get(d, k, default) reads without changing anything, which is why the counting line adds 1 and stores the sum itself: counts[ch] = get(counts, ch, 0) + 1.

  2. Question 2 of 2 How many entries does Dict(1 => "a", 1.0 => "b", 2 => "c") have?

    Type a number.

    A hint

    Which of the keys does isequal treat as the same?

    Show the answer: Question 2 of 2

    Answer: 2 entries

    isequal(1, 1.0) is true and hash(1) == hash(1.0), so 1 and 1.0 are one key, holding "b", the value written last; 2 is the second key.

The order of keys is unspecified, and Julia 1.13 changed it

The five cities went in as mumbai, delhi, chennai, kolkata and pune, and no release this track runs gave them back in that order:

dict_order.jl: the order of five keys, before and after the table grows Julia · dict_order.jl
# The order in which a Dict hands out its keys: not the order they went in, and not the same on every release.
cities = ["mumbai", "delhi", "chennai", "kolkata", "pune"]
d = Dict(c => length(c) for c in cities)
println("inserted:      ", join(cities, ", "))
println("keys(d):       ", join(keys(d), ", "))
println("hash(42):      ", hash(42))

grown = copy(d)
for k in 1:12
    grown["extra$k"] = k                         # the table grows and the keys are placed again
end
println("after growing: ", join(filter(in(cities), collect(keys(grown))), ", "))

println("sorted:        ", join(sort!(collect(keys(d))), ", "))
try
    println("sort(keys(d)): ", join(sort(keys(d)), ", "))
catch err
    println("sort(keys(d)): ", typeof(err))
end
same = Dict(c => length(c) for c in reverse(cities))
println("d == same:     ", d == same)            # == compares entries, not order
dict_order.jl, run with Julia 1.13.1, 1.12.7 and the 1.10 LTS Python · order_session.py
from session import files, run, use

# The same five keys on the current release, on 1.12.7 and on the 1.10 LTS: Julia 1.13 changed the hash function.
files("dict_order.jl")
run("""
julia --version
julia dict_order.jl
""")
use("1.12.7")
run("""
julia --version
julia dict_order.jl
""")
use("1.10.12")
run("""
julia --version
julia dict_order.jl
""")

Output

$ julia --version
julia version 1.13.1
$ julia dict_order.jl
inserted:      mumbai, delhi, chennai, kolkata, pune
keys(d):       delhi, chennai, kolkata, mumbai, pune
hash(42):      16440345385870586648
after growing: chennai, kolkata, delhi, mumbai, pune
sorted:        chennai, delhi, kolkata, mumbai, pune
sort(keys(d)): chennai, delhi, kolkata, mumbai, pune
d == same:     true
$ julia --version
julia version 1.12.7
$ julia dict_order.jl
inserted:      mumbai, delhi, chennai, kolkata, pune
keys(d):       kolkata, delhi, mumbai, chennai, pune
hash(42):      1098236396662648698
after growing: chennai, pune, kolkata, mumbai, delhi
sorted:        chennai, delhi, kolkata, mumbai, pune
sort(keys(d)): MethodError
d == same:     true
$ julia --version
julia version 1.10.12
$ julia dict_order.jl
inserted:      mumbai, delhi, chennai, kolkata, pune
keys(d):       kolkata, delhi, mumbai, chennai, pune
hash(42):      1098236396662648698
after growing: chennai, pune, kolkata, mumbai, delhi
sorted:        chennai, delhi, kolkata, mumbai, pune
sort(keys(d)): MethodError
d == same:     true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py

The order comes from where each key sits. Julia’s Dict keeps its entries in a table of slots, and the source of 1.13.1 shows how a key is placed: its hash, cut down to the size of the table, picks a slot, and when that slot is taken the key goes into the next free one, a method called linear probing. Iterating walks the slots from the first to the last. The figure below is a small table of 8 slots built the same way, with one simplification so you can check every step by hand: the hash of a whole number is the number itself, so a key’s first slot is the key mod 8.

Interactive · step through it Five keys placed in a table of 8 slots by linear probing Each key goes to slot k mod m; watch what happens when two keys want the same slot.
Hash table of 8 slots, 0 keys. All empty.01234567
  • being compared or looked at
  • being moved or written

Pseudo-code

  1. insert(k): for i from 0: s ← (k mod m + i) mod m
  2. if slot s is empty or a tombstone: put k there; stop
  3. find(k): probe the same sequence until k or an empty slot
  4. delete(k): find k, then leave a tombstone in its slot

A table of 8 slots with linear probing: on a collision, try the next slot, then the one after. The home slot of a key k is k mod 8.

When it finishes: 5 keys stored.

Slots probed 0
Collisions 0
Load factor 0.00

The keys went in as 23, 8, 41, 30 and 15 and sit in the slots as 8, 41, 15, 30 and 23, the order a loop over the table would give. Type 16 as the table size and run it again: four of the five keys move, and this time the slot order happens to match the order of insertion. A real Dict changes in both ways. When it grows, every key is placed again in a larger table: after 12 more keys the cities came out as chennai, kolkata, delhi, mumbai, pune on 1.13.1. And a new hash function places every key anew: Julia 1.13 hashes strings with a new function, RapidhashNano, and mixes small values such as integers differently too, so hash(42) printed 16440345385870586648 there but 1098236396662648698 on 1.12.7 and 1.10.12.

Timeline · 4 events in order Dictionaries and sets across Julia releases Walk through four releases and see which calls arrived and when the order of keys changed.
  1. 1.5

    mergewith arrives new function

    mergewith combines the values of keys that two dictionaries share with a function you give, as in mergewith(+, a, b).

  2. 1.10 LTS

    The long-term support release

    The five cities came out as kolkata, delhi, mumbai, chennai, pune. sort(keys(d)) threw a MethodError, and in! did not exist.

  3. 1.11

    in! arrives new function

    in!(x, s) returns true if x is already in the set s, and adds it otherwise, in one call.

  4. 1.13

    A new hash function changed behaviour

    Strings and other bytes are hashed with RapidhashNano and small values are mixed differently, so the same keys came out as delhi, chennai, kolkata, mumbai, pune. sort(keys(d)) now collects the keys first, and merge(+, a, b) is deprecated in favour of mergewith.

The timeline has 4 events, from 1.5 to 1.13.

Code that must give the same result everywhere sorts what it prints or compares. sort!(collect(keys(d))) worked on all three releases, while sort(keys(d)) worked on 1.13.1 and threw a MethodError on 1.12.7 and 1.10.12, so it ties code to the newest release. d == same was true on every release, because == compares the entries, not the order. When the order of insertion matters, the OrderedDict of the OrderedCollections.jl package keeps it, as its documentation says.

A test that compares printed dictionaries

Wrong: a test that prints a Dict and compares the text with a saved file. It passed on 1.12.7 and fails on 1.13.1 with the same entries. Right: compare the dictionaries with ==, or compare sort!(collect(d)).

merge and mergewith combine dictionaries

Two days of rain-gauge readings, in millimetres, sit in two dictionaries that share two gauges. Combining them raises one question: what happens to a key that is in both? merge keeps the value of the dictionary further right, and mergewith combines the two values with a function you give:

merge_counts.jl: two days of rain, merged two ways Julia · merge_counts.jl
# Two days of rain-gauge readings, in mm, combined in two ways.
monday = Dict("Pune" => 12, "Nagpur" => 0, "Kochi" => 31)
tuesday = Dict("Pune" => 8, "Kochi" => 22, "Nashik" => 5)

latest = merge(monday, tuesday)                  # a key in both: the later Dict's value wins
println("merge:     ", sort!(collect(latest)))
totals = mergewith(+, monday, tuesday)           # a key in both: the values are combined with +
println("mergewith: ", sort!(collect(totals)))
mergewith!(+, monday, Dict("Nagpur" => 3))       # the ! form changes monday itself
println("monday:    ", sort!(collect(monday)))
old_merge.jl: the older form, merge with a combining function Julia · old_merge.jl
# merge with a combining function: still runs on Julia 1.13, which deprecates it in favour of mergewith.
totals = merge(+, Dict("Pune" => 12), Dict("Pune" => 8))
println(totals)
merge_counts.jl and old_merge.jl, run with Julia 1.13.1 Python · merge_session.py
from session import files, run

# merge and mergewith on two days of readings, then the older merge(+, ...) form with deprecation warnings switched on.
files("merge_counts.jl", "old_merge.jl")
run("""
julia merge_counts.jl
julia --depwarn=yes old_merge.jl
""")

Output

$ julia merge_counts.jl
merge:     ["Kochi" => 22, "Nagpur" => 0, "Nashik" => 5, "Pune" => 8]
mergewith: ["Kochi" => 53, "Nagpur" => 0, "Nashik" => 5, "Pune" => 20]
monday:    ["Kochi" => 31, "Nagpur" => 3, "Pune" => 12]
$ julia --depwarn=yes old_merge.jl
┌ Warning: `merge(combine::Callable, d::AbstractDict, others::AbstractDict...)` is deprecated, use `mergewith(combine, d, others...)` instead.
│   caller = top-level scope at old_merge.jl:2
└ @ Core ~/demo/old_merge.jl:2
Dict("Pune" => 20)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 merge_session.py

Follow mergewith(+, monday, tuesday) as it builds its result:

Interactive · stage by stage How mergewith(+, monday, tuesday) combines two days of rain Step down the column. Each key ends up in the result once; only keys in both dictionaries meet the function.
How mergewith(+, monday, tuesday) combines two days of rain, step 4 of 4. Showing monday: 3 gauges, tuesday: 3 gauges, only in monday, only in tuesday, in both, a new Dict. In focus: a new Dict.monday: 3 gaugesPune 12, Nagpur 0, Kochi 31tuesday: 3 gaugesPune 8, Kochi 22, Nashik 5only in mondayNagpur 0, kept as it isonly in tuesdayNashik 5, kept as it isin bothPune 12 + 8, Kochi 31 + 22a new DictKochi 53, Pune 20, …

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Two dictionaries. monday and tuesday hold rainfall in mm for three gauges each. Two gauges, Pune and Kochi, appear in both.
  2. Keys of one side. A key in only one dictionary goes into the result with its value: Nagpur with 0 from monday, Nashik with 5 from tuesday.
  3. Keys of both sides. For a key in both, mergewith calls the function on the two values: + gives Pune 12 + 8 = 20 and Kochi 31 + 22 = 53.
  4. The result. The run printed Kochi => 53, Nagpur => 0, Nashik => 5, Pune => 20. merge(monday, tuesday) would have kept the later values instead: Pune 8 and Kochi 22.

merge gave Pune 8 and Kochi 22, tuesday’s values, while mergewith(+, …) added them, Pune 20 and Kochi 53. A key in only one dictionary goes into either result unchanged. Both return a new dictionary; mergewith! changes its first argument instead, and added Nagpur’s 3 mm to monday itself. mergewith needs Julia 1.5 or later, which every release this track runs has.

The second program uses an older form, merge(+, a, b). It still ran on 1.13.1 and gave Dict("Pune" => 20), but with deprecation warnings switched on, --depwarn=yes, Julia printed that it is deprecated and to use mergewith instead, as the 1.13 release notes say. Julia prints nothing by default, so such calls are easy to miss until a future release removes them.

Checkpoint

Check: Iteration order and the 1.13 change · merge and mergewith

  1. Question 1 of 2 Which line prints the keys of a Dict d in the same order on Julia 1.13.1, 1.12.7 and 1.10.12?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: println(sort!(collect(keys(d))))

    Sort before you print or compare. sort(keys(d)) collects the keys for you only from Julia 1.13 on, so sort!(collect(keys(d))) is the form that runs everywhere.

  2. Question 2 of 2 What is mergewith(+, Dict("Pune" => 12, "Kochi" => 31), Dict("Pune" => 8))["Pune"]?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 20 mm

    Pune is in both dictionaries, so mergewith calls + on the two values: 12 + 8 = 20. merge would have kept the later value, 8.

A set keeps each value once

A set: A collection that holds each value at most once, compared with isequal, such as Set(["A-101", "B-207"]). It answers x in s with a hash lookup, and the order of its elements cannot be relied on. is a collection that holds each value at most once, by the same isequal rule as the keys of a dictionary. In the source of Julia 1.13.1 a Set is a Dict whose values are all nothing, and x in s is haskey on that dictionary: a hash lookup, not a search through a list. Its order is unspecified too: the documentation calls it an implementation detail that cannot be relied on. This program compares two audits of a lab’s instruments by their serial numbers:

inventory_sets.jl: two audits compared with set operations Julia · inventory_sets.jl
# Serial numbers of the instruments in a lab before and after an audit, as sets.
before = Set(["A-101", "A-102", "B-207", "C-330"])
after = Set(["A-101", "B-207", "C-330", "D-412", "B-207"])   # B-207 was counted twice
println(length(after), " instruments after the audit")
show_set(label, s) = println(rpad(label, 22), join(sort!(collect(s)), ", "))
show_set("in both:", intersect(before, after))
show_set("missing after audit:", setdiff(before, after))
show_set("new in the audit:", setdiff(after, before))
show_set("in exactly one:", symdiff(before, after))
show_set("in either:", union(before, after))
println("\"A-101\" in after: ", "A-101" in after, ", Set([\"A-101\"]) ⊆ before: ", Set(["A-101"]) ⊆ before)
seen_twice.jl: flagging values seen twice with in! Julia · seen_twice.jl
# Flagging serial numbers that were counted twice, with in!, which tests and inserts in one call.
seen = Set{String}()
for serial in ["A-101", "B-207", "A-101", "C-330", "B-207"]
    if in!(serial, seen)                         # true if it was there already; otherwise it is added
        println("counted twice: ", serial)
    end
end
println(length(seen), " different serial numbers")
inventory_sets.jl and seen_twice.jl, run with Julia 1.13.1 and with the 1.10 LTS Python · sets_session.py
from session import files, run, use

# Set operations on two audits, then in! on the current release and on the 1.10 LTS, which does not have it.
files("inventory_sets.jl", "seen_twice.jl")
run("""
julia --version
julia inventory_sets.jl
julia seen_twice.jl
""")
use("1.10.12")
run("""
julia --version
!julia seen_twice.jl
""")

Output

$ julia --version
julia version 1.13.1
$ julia inventory_sets.jl
4 instruments after the audit
in both:              A-101, B-207, C-330
missing after audit:  A-102
new in the audit:     D-412
in exactly one:       A-102, D-412
in either:            A-101, A-102, B-207, C-330, D-412
"A-101" in after: true, Set(["A-101"]) ⊆ before: true
$ julia seen_twice.jl
counted twice: A-101
counted twice: B-207
3 different serial numbers
$ julia --version
julia version 1.10.12
$ julia seen_twice.jl
ERROR: LoadError: UndefVarError: `in!` not defined
Stacktrace:
 [1] top-level scope
   @ ~/demo/seen_twice.jl:4
in expression starting at ~/demo/seen_twice.jl:3
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sets_session.py

Step down the operations the run printed:

Interactive · stage by stage Two audits of a lab, compared with set operations Step down the column. Each stage is one operation on the sets before and after.
Two audits of a lab, compared with set operations, step 6 of 6. Showing before: 4 serial numbers, after: 4 serial numbers, intersect(before, after), setdiff(before, after), setdiff(after, before), union(before, after). In focus: union(before, after).before: 4 serial numbersA-101, A-102, B-207, C-330after: 4 serial numbersB-207 was counted twiceintersect(before, after)A-101, B-207, C-330setdiff(before, after)A-102: missing nowsetdiff(after, before)D-412: newunion(before, after)5 different instruments

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Before the audit. before holds the four serial numbers on the lab's list.
  2. After the audit. The audit wrote down five numbers, B-207 twice. A set keeps each value once, so after has 4 elements, as the run printed.
  3. In both. intersect keeps the values that are in both sets: the three instruments that were on the list and were found.
  4. Missing. setdiff(before, after) keeps what is in before but not in after: A-102 was not found.
  5. New. The other way round, setdiff(after, before) gives D-412, found but not on the list. The order of the arguments decides the question.
  6. In either. union gives every instrument seen at least once, five. symdiff gives the two that are in exactly one set, A-102 and D-412.

The audit wrote down five numbers and the set kept four, because B-207 was counted twice. intersect, setdiff and union answered three different questions, and the order of the arguments of setdiff decided which one: A-102 is missing from the audit, D-412 is new to the list. "A-101" in after and Set(["A-101"]) ⊆ before, the subset test also written issubset, both gave true.

in!(serial, seen) tests and inserts in one call: it returned true for A-101 and B-207 the second time they appeared, and the loop ended with 3 different numbers. The 1.10.12 run stopped at that line with UndefVarError: `in!` not defined, because in! needs Julia 1.11. Code for the LTS writes the two steps out: serial in seen ? println(…) : push!(seen, serial).

Compare Two Lists Paste two lists of IDs to see what is in one but not the other, and what they share, as setdiff and intersect do.

Checkpoint

Check: Sets and set operations

  1. One question Which call lists the instruments that the audit found but that were not on the list before it?

    Choose one answer.

    Show the answer: One question

    Answer: setdiff(after, before), which gave D-412

    The order of the arguments of setdiff decides the question; intersect, union and symdiff give the same result either way round.

Practise: an inventory difference

Write inventory_diff

Write inventory_diff(old, new) for two Dict{String, Int} inventories, item name to quantity, that returns the named tuple (added, removed, changed), each a sorted vector of item names. With old = Dict("pipette" => 4, "beaker" => 10, "flask" => 2) and new = Dict("pipette" => 4, "beaker" => 8, "burette" => 1), it should give added ["burette"], removed ["flask"] and changed ["beaker"]. Run it on your machine with Julia 1.13 and 1.10, and make sure it uses no call that only the newer release has.

Check yourself on counting, keys, order, merging and sets. The session opens with the part of the lesson your answers have found hardest, and each wrong choice says what it overlooked:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 What does freq["snow"] do when "snow" is not a key of freq?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: It throws KeyError: key "snow" not found

    Use d[k] when a missing key is a mistake, and get with a default, or haskey first, when it is expected.

  2. Question 2 of 15 The first time "Pune" appears, what does push!(get!(by_station, "Pune", Float64[]), 24.8) do?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: It stores an empty vector under "Pune", then pushes 24.8 into that stored vector

    get! groups values in one line: the first call for a key creates its vector, the later calls add to it, as "Pune" => [25.2, 24.8] showed.

  3. Question 3 of 15 Words counted with freq[w] = get(freq, w, 0) + 1 give "The" and "the" as two keys. What do you change?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Lowercase the text, or each word, before counting, as the weather note was

    The note was lowercased with lowercase(note) before split, which is why "The" and "the" were counted as one key with 3.

  4. Question 4 of 15 Which function does a Dict use to decide that two keys are the same key?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: isequal, together with hash

    isequal decides equality and hash must agree with it, so values that are isequal always land in the same slot.

  5. Question 5 of 15 How many entries does Dict(0.0 => "a", -0.0 => "b") have?

    Type a number.

    Show the answer: Question 5 of 15

    Answer: 2 entries

    isequal(-0.0, 0.0) is false, so they are two keys, although -0.0 == 0.0 is true. The run's four pairs gave three entries for the same reason.

  6. Question 6 of 15 A Dict is keyed by vectors, and later code runs push! on one of those vectors. What happens to its entry?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: It stays in the Dict and counts in length, but no lookup can find it

    Never change a key after storing it. Use immutable keys, such as a tuple, which the run found, or a copy of the vector made with copy before it is stored.

  7. Question 7 of 15 In what order does keys(d) hand out the keys of a Dict?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: In the order of the slots they occupy, which the documentation leaves unspecified

    The order is a by-product of hashing, so it changes when the table grows or the hash function changes, as it did in Julia 1.13.

  8. Question 8 of 15 A script calls sort(keys(d)). On which of the three releases did that line work?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: Only on Julia 1.13.1

    A call that works only on the newest release ties the code to it; the collect-then-sort form works everywhere.

  9. Question 9 of 15 A test checks @test string(totals) == "Dict(\"a\" => 1, \"b\" => 2)" and fails after an upgrade to 1.13, with the same totals. Which change makes it hold on every release?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: @test totals == Dict("a" => 1, "b" => 2)

    Compare values, not their printed form; when text is needed, print sort!(collect(d)).

  10. Question 10 of 15 What is merge(Dict(:a => 1), Dict(:a => 2))[:a]?

    Type a number.

    Show the answer: Question 10 of 15

    Answer: 2

    merge keeps the value of the dictionary further right for a key that is in both, as tuesday's Pune 8 replaced monday's 12.

  11. Question 11 of 15 How does mergewith!(+, monday, extra) differ from mergewith(+, monday, extra)?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: The ! form changes monday itself; the other returns a new Dict and leaves monday alone

    Use mergewith! to update a running total in place, and mergewith when the inputs must stay as they were.

  12. Question 12 of 15 Code calls merge(+, a, b) and prints no warning when run with julia script.jl on 1.13.1. What should you do?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Replace it with mergewith(+, a, b), because the call is deprecated and the warning shows only with --depwarn=yes

    Run tests with --depwarn=yes, or error, to find deprecated calls before they are removed.

  13. Question 13 of 15 How many elements does Set(["A-101", "B-207", "B-207"]) have?

    Type a number.

    Show the answer: Question 13 of 15

    Answer: 2 elements

    A set keeps each value once, as the audit's five written numbers became a set of 4.

  14. Question 14 of 15 What does in!(x, s) return when x is not yet in the set s?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: false, after adding x to s

    in! answers whether x was already in the set, and when it was not, it adds x before answering false, so the test and the insertion are one call. It needs Julia 1.11.

  15. Question 15 of 15 A function must run on the 1.10 LTS and flag values seen twice in a loop. Which loop body works there?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: x in seen ? flag(x) : push!(seen, x)

    Write the test and the insertion as two steps when the code must run on the LTS; use in! from Julia 1.11 on.

Interview questions

Warm-up (fresher to 2–5 years): how does a Julia Dict decide that two keys are the same, and what does that mean for 1 and 1.0?

A Dict compares keys with isequal and finds them by hash, and the two agree: values that are isequal have the same hash. isequal(1, 1.0) is true and hash(1) == hash(1.0) is true, so 1 and 1.0 are one key: a dictionary built from 1 => :a and 1.0 => :b has a single entry, and d[1] returns :b, the value written last. isequal is stricter than == for floating-point zeros, so -0.0 and 0.0 are two keys, and it treats NaN as equal to itself, so NaN can be found. Strings compare exactly, so “Pune” and “pune” are different keys, and a string is never equal to a symbol of the same name.

Two harder questions follow, about a test that broke when Julia 1.13 changed the hash function and about a cache whose entries go missing. Decide your answer first, then open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    A test writes a Dict of station totals to text and compares it with a saved file. It passed on Julia 1.12 and fails on 1.13, although every total is the same. Why, and how do you fix the test and the code that writes the file?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A cache maps parameter vectors such as [a, b] to computed results. After a while it misses entries that were stored, and length(cache) keeps growing. What is happening, and what would you use as the key instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • get(d, k, default) gives a value or a default, get! also stores the default, d[k] throws a KeyError for a missing key, and delete! removes one.
  • A Dict compares keys with isequal and finds them by hash, so 1 and 1.0 are one key, -0.0 and 0.0 are two, and a key that changes after it is stored can no longer be found.
  • The order of a Dict follows its slots, so it changes when the table grows and when the hash changes, as it did in Julia 1.13; sort with sort!(collect(keys(d))) and compare with ==.
  • merge keeps the later value of a shared key, mergewith(f, a, b) combines the two values, and merge(f, a, b) is deprecated from Julia 1.13.
  • A Set keeps each value once; intersect, setdiff, union and ⊆ compare collections, and in!, from Julia 1.11, tests and inserts in one call.

Terms in this lesson

dictionary
A collection of entries, each a key and a value, that finds the value of a key without searching the others: Julia's Dict. Each key appears once, and keys are compared with isequal.
hash value
The number hash(x) computes from a value, which a Dict or a Set uses to choose where to store it. Values that are isequal have the same hash; Julia 1.13 changed the hash of many values, hash(42) among them.
set
A collection that holds each value at most once, compared with isequal, such as Set(["A-101", "B-207"]). It answers x in s with a hash lookup, and the order of its elements cannot be relied on.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.