3  Keeping rows

Which of the year’s orders happened in the West? Most questions about a table begin this way, by wanting fewer rows than you have. keep is the verb: it takes a condition, asks it of every row, and keeps the rows where the answer is yes.

run('
  sales
    then keep where ([region] is "West")
')
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-04-11 West Widget 2 50 20
2026-07-08 West Sprocket 6 90 72
2026-10-02 West Widget 5 125 50
sales |> keep(region == "West")
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-04-11 West Widget 2 50 20
2026-07-08 West Sprocket 6 90 72
2026-10-02 West Widget 5 125 50
sales >> keep(col.region == "West")
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-04-11 West Widget 2 50 20
2026-07-08 West Sprocket 6 90 72
2026-10-02 West Widget 5 125 50

sales then keep where [region] is "West"

Six of the fifteen rows survive. The other nine were not deleted from anything; sales is untouched, and the pipeline handed back a new, smaller table. Every verb in this grammar works that way, so no step can damage the table a colleague is also using.

3.1 Combining conditions

Conditions join with and and or, and a negation reverses one. Ask for the larger orders that were still cheap to fulfill:

run('
  sales
    then keep where (([revenue] >= 150) and ([cost] < 100))
')
date region product quantity revenue cost
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
sales |> keep(revenue >= 150 & cost < 100)
date region product quantity revenue cost
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
sales >> keep((col.revenue >= 150) & (col.cost < 100))
date region product quantity revenue cost
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80

Python needs the parentheses and R does not, because Python’s & binds more tightly than its ==. That rule is Python’s rather than the grammar’s: the preface holds the short list of rules Python brings of its own, and that list is the whole of the subject.

3.2 The words where the hosts disagree

Look at what the text form did to the R sentences above: == became is, and & became and. That is not a style choice; it is a decision about which words can be in a grammar at all.

A symbol that means different things in different tools is a word this grammar refuses to own. = assigns in one language, compares in another, and half the world’s beginners have confused the two. So the text form spells every contested symbol as the English word that means only one thing, and each binding translates its host’s habit into that word:

you write in R you write in Python the grammar’s word
== == is
!= != is not
& & and
\| \| or
! ~ not
%in% .is_in([...]) in { }
is.na(x) .is_missing() [x] is missing

You keep writing your language’s habits, and the grammar keeps one meaning under them. When a pipeline moves to a database cell or a colleague’s screen, it is the unambiguous spelling that travels.

3.3 One verb, both directions

There is no separate verb for dropping rows. Removing the rows where something is true is keeping the rows where it is false, and one verb says both:

run('
  sales
    then keep where (not ([product] is "Widget"))
')
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-02-14 North Doohickey 2 80 50
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-06-19 East Sprocket 10 150 120
2026-07-08 West Sprocket 6 90 72
2026-09-15 East Gadget 1 60 40
sales |> keep(!(product == "Widget"))
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-02-14 North Doohickey 2 80 50
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-06-19 East Sprocket 10 150 120
2026-07-08 West Sprocket 6 90 72
2026-09-15 East Gadget 1 60 40
sales >> keep(~(col.product == "Widget"))
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-02-14 North Doohickey 2 80 50
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-06-19 East Sprocket 10 150 120
2026-07-08 West Sprocket 6 90 72
2026-09-15 East Gadget 1 60 40

This is the first appearance of a rule you will meet on every page: if the grammar can already say something, it does not grow a second way to say it. A drop verb would cost a word, and every reader who met drop(x) beside keep(!x) would have to learn, and then remember, that they are the same thing. The grammar spends its words on meanings, not on synonyms.

3.4 What travels with it

  • where is how the text form marks the condition, as in the gloss above. You never type it in R or Python; the condition sits inside the verb.
  • The condition language travels. A when (choosing a value by a question) takes the conditions keep takes, unchanged. A pick where (choosing columns by a question) joins and negates its questions the same way, and asks them about a column’s name and kind rather than its values.
  • and, or, not, is, in, missing are the grammar’s own words for the contested symbols, and the table above is their whole story.

3.5 What it refuses

A condition that can never be true is a mistake, not a preference, and the grammar says so before anything runs. Text is never greater than a number:

collect(sales |> keep(region > 100))
Error:
! 
illegal: this compares text with a number, which can never match. Convert one of them first
  |
2 |   then keep where ([region] > 100)
  |                     ^^^^^^^^^^^^^
try:
    collect(sales >> keep(col.region > 100))
except GodError as refusal:
    print(refusal)

illegal: this compares text with a number, which can never match. Convert one of them first
  |
2 |   then keep where ([region] > 100)
  |                     ^^^^^^^^^^^^^

Nothing was scanned and no rows came back empty. The pipeline was refused whole, at the step that is wrong, with the fix in the message. A tool that instead answered with zero rows would have told you something false about your data, quietly, and you would have believed it.