Arguments: positional, keyword, splat and block¶
Level: 201 · a reader who knows Python's *args and **kwargs
One line: A Ruby parameter list has eight kinds of slot — required, optional, *rest, post-required, key:, key: default, **opts and &blk — plus ... and anonymous forwarding — and a Hash argument never turns into keywords by itself.
The positional part of a Ruby signature is matched by position only: a name in a def is never something a caller can say, so two(b: 2, a: 1) does not fill a and b, it passes one Hash. Keywords are a separate channel, declared with a trailing colon (k: required, d: 4 defaulted, **opts for the rest), and the two channels do not leak into each other (the Ruby 3.0 "keyword separation" (Not machine-checked here.)): kw({k: 1}) is a positional Hash and fails, kw(**{k: 1}) splats it into keywords and works. A required positional may even come after the splat (def f(a, b = 2, *rest, z)), which Python cannot express. The block is its own slot (&blk), and ... forwards all three channels at once; a bare *, ** or & in the parameter list can be forwarded without a name (new in Ruby 3.2 (Not machine-checked here.)). When a call does not fit, the error is an ArgumentError with one of three stable messages.
Verified output of arguments_positional_keyword_and_splat_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. required, optional, *rest, post-required: def f(a, b = 2, *rest, z)
f(1, 9) -> [1, 2, [], 9]
f(1, 2, 3, 4, 5) -> [1, 2, [3, 4], 5]
2. introspected: method(:f).parameters -> [[:req, :a], [:opt, :b], [:rest, :rest], [:req, :z]]
3. keywords: def g(a, k:, d: 4, **opts)
g(1, k: 2) -> [1, 2, 4, {}]
g(1, k: 2, d: 5, x: 6) -> [1, 2, 5, {x: 6}]
4. a block parameter: h { |x| x * 2 } -> 6
5. ... forwards everything: fwd(1, 2, k: 3) { :b } -> [[1, 2], {k: 3}, :b]
6. anonymous *, ** and &: anon(1, k: 2) { :b } -> [[1], {k: 2}, :b]
its parameters -> [[:rest, :*], [:keyrest, :**], [:block, :&]]
7. too few positionals: f(1) -> ArgumentError: wrong number of arguments (given 1, expected 2+)
8. a missing keyword: kw() -> ArgumentError: missing keyword: :k
9. an unknown keyword: kw(k: 1, z: 2) -> ArgumentError: unknown keyword: :z
10. a Hash is not keywords: kw({k: 1}) -> ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: k)
but ** splats it: kw(**{k: 1}) -> [1, 4]
11. positionals have no names: two(b: 2, a: 1) -> ArgumentError: wrong number of arguments (given 1, expected 2)
the Hash lands in one slot: pos(a: 1) -> {a: 1} (a Hash)
12. arity: f -3, g 2, kw 1, two 2, anon -1
Reading the output¶
Row 1 is the positional channel in full: with two arguments, a and the post-required z are filled first and b keeps its default; with five, b takes the second, z the last, and *rest whatever is left. Row 2 is the same signature as Method#parameters reports it — :req, :opt, :rest, :req — and row 3 adds the keyword channel: :keyreq, :key, :keyrest. Rows 4 to 6 are the block slot and forwarding: &blk reifies the block, ... passes positionals, keywords and the block on untouched, and the anonymous *, **, & do the same with no names to invent (their parameters entries are literally :*, :** and :&).
Rows 7 to 9 are the three messages to recognise: "wrong number of arguments (given 1, expected 2+)", "missing keyword: :k", "unknown keyword: :z". Row 10 is the keyword-separation rule: a braced Hash is one positional argument, so kw({k: 1}) fails with a message that even names the missing keyword, while ** splats it into the keyword channel. Row 11 is its mirror image: positionals have no names, so two(b: 2, a: 1) is one Hash where two arguments were expected, and pos(a: 1) receives that Hash as a. Row 12: arity is the count of required arguments, negative when the method takes optional ones (-3 for f means at least two, -(2+1)); a required keyword counts as one.
Compared with Python¶
Python's positional and keyword channels overlap: any parameter declared without a marker can be passed either by position or by name, so two(b=2, a=1) works (row 11) — and you need / to forbid it (pos_only). A bare name after *rest is not a post-required positional but keyword-only, which is why the same def f(a, b=2, *rest, z) rejects f(1, 9) in row 1 and needs z=5 — Ruby's post-required positional has no Python spelling. Rows 3 and 10 agree with Ruby: keyword-only after *, **opts for the rest, a dict is one positional argument and ** splats it. Python has no block slot (a callable is an ordinary argument, rows 4 and 5) and no anonymous forwarding: *args, **kwargs must be named to be passed on (row 6). Every failed call is a TypeError; the twin prints only the type, because CPython rewords these messages between releases. There is no arity; inspect.signature gives the parameters and their kinds (rows 2 and 12).
Verified output of arguments_positional_keyword_and_splat_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. required, default, *rest, then a name: def f(a, b=2, *rest, z) -- z is keyword-only
f(1, 9) -> TypeError (z was not given)
f(1, 2, 3, 4, z=5) -> [1, 2, (3, 4), 5]
2. introspected: inspect.signature(f) -> (a, b=2, *rest, z)
3. keyword-only after *: def g(a, *, k, d=4, **opts)
g(1, k=2) -> [1, 2, 4, {}]
g(1, k=2, d=5, x=6) -> [1, 2, 5, {'x': 6}]
4. a callable parameter: h(lambda x: x * 2) -> 6
5. *args, **kwargs forward: fwd(1, 2, k=3) -> [(1, 2), {'k': 3}] (no block slot)
6. anonymous *, ** and &: no counterpart: *args and **kwargs must be named to be forwarded
fwd's parameters -> (*args, **kwargs)
7. too few positionals: f(1) -> TypeError
8. a missing keyword: kw() -> TypeError
9. an unknown keyword: kw(k=1, z=2) -> TypeError
10. a dict is not keywords: kw({'k': 1}) -> TypeError
but ** splats it: kw(**{'k': 1}) -> [1, 4]
11. positionals have names: two(b=2, a=1) -> [1, 2]
unless marked with /: pos_only(a=1) -> TypeError; pos(a=1) -> 1
12. no arity; parameter counts: f 4, g 4, kw 2, two 2, fwd 2
Try it¶
- Write
order(item, qty = 1, *extras, priority:, note: "")returning[item, qty, extras, priority, note]. Call it four correct ways (only the required parts; with a quantity; with extras; with a note) and three wrong ways (nopriority:, an unknownsize:keyword, noitem), printing each result orArgumentErrormessage. - Write
call_twice(name, ...)that doessend(name, ...)two times and returns both results in an array. Check with a target that takes positionals, keywords and a block that all three channels arrive intact both times —...may follow a leading named parameter (Not machine-checked here.).
Solution to 1
arguments_positional_keyword_and_splat_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: one method with a required, an optional, a splat, a required keyword
# and a defaulted keyword -- called four good ways and three bad ones.
def order(item, qty = 1, *extras, priority:, note: "")
[item, qty, extras, priority, note]
end
def attempt
yield.inspect
rescue ArgumentError => e
"#{e.class}: #{e.message}"
end
puts "1. order(:tea, priority: :low) -> #{attempt { order(:tea, priority: :low) }}"
puts "2. order(:tea, 2, priority: :low) -> #{attempt { order(:tea, 2, priority: :low) }}"
puts "3. order(:tea, 2, :milk, :sugar, priority: :high) -> #{attempt { order(:tea, 2, :milk, :sugar, priority: :high) }}"
puts "4. order(:tea, priority: :low, note: \"decaf\") -> #{attempt { order(:tea, priority: :low, note: "decaf") }}"
puts "5. order(:tea) -> #{attempt { order(:tea) }}"
puts "6. order(:tea, priority: :low, size: :large) -> #{attempt { order(:tea, priority: :low, size: :large) }}"
puts "7. order(priority: :low) -> #{attempt { order(priority: :low) }}"
Verified output of arguments_positional_keyword_and_splat_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. order(:tea, priority: :low) -> [:tea, 1, [], :low, ""]
2. order(:tea, 2, priority: :low) -> [:tea, 2, [], :low, ""]
3. order(:tea, 2, :milk, :sugar, priority: :high) -> [:tea, 2, [:milk, :sugar], :high, ""]
4. order(:tea, priority: :low, note: "decaf") -> [:tea, 1, [], :low, "decaf"]
5. order(:tea) -> ArgumentError: missing keyword: :priority
6. order(:tea, priority: :low, size: :large) -> ArgumentError: unknown keyword: :size
7. order(priority: :low) -> ArgumentError: wrong number of arguments (given 0, expected 1+; required keyword: priority)
See also¶
- Default arguments are evaluated on every call — what the
b = 2slot does on each call, and the Python trap it avoids - Blocks are not objects — the
&blkslot,yieldandblock_given? - Common error messages — rows 7 to 10 in the library-wide table of errors
- Destructuring assignment — the splat on the receiving side of an assignment
- Ruby versions: what changed — keyword separation (3.0),
...(2.7), anonymous*/**/&(3.2) - Ruby docs: Methods — arguments ↗ — the reference for every parameter kind on this page
- Python tutorial: more on defining functions ↗ — defaults, keyword arguments,
/and*markers,*argsand**kwargs - A variable is a reference, not a box — Assignment copies a reference, never the object, so after
b = aa mutating method such as<<onbis… - Procs curry and compose —
Proc#curryturns an n-argument lambda into a chain of one-argument lambdas,>>and<<compose any… - Procs and lambdas differ in arity and
return— Every proc and lambda is aProc, but a lambda checks its argument count andreturns from itself, while… - Past the end, an array gives nil, not an error —
a[10]on a four-element array isnil,a[5] = 99pads the gap withnil, and a slice isa[1..2]…