skip to content

In Selenium's Python client, why must a locator given to expected_conditions be wrapped in a tuple?

level: juniorimportance: should knowfreq 50%

answer

  1. Look at how many arguments each call takes
  2. One helper parameter, two locator halves
  3. The double parentheses are not a typo
  4. Helpers ending in _located want one value
  5. A pair as one argument is a tuple

basics

~20 s

Because those helpers take one locator argument, while find_element takes two. The pair goes in as a single value, and a single value holding two things is a tuple, which is why the call shows double parentheses.

solid answer

~30 s

The Python driver method is `find_element(by, value)` - two positional arguments. The helpers in `selenium.webdriver.support.expected_conditions` instead take one locator, because each returns a callable the wait evaluates later and it has to carry the locator as a single thing. So the same pair becomes a tuple: `EC.visibility_of_element_located((By.ID, "term-average"))`, with the inner parentheses building the tuple and the outer ones making the call. Drop them and Python raises a `TypeError` about one positional argument being expected. Java never meets this because `By.id("term-average")` is already a single object.

go deeper

for a junior

Recall the shape: two arguments for find_element, one tuple for a condition helper. If a condition call complains about positional arguments, add the inner parentheses.

for a middle

Explain why the shapes differ - the helper returns a callable that must carry the locator as one value - and point out that helpers named _located are the ones taking a tuple.

for a senior

Show how you would keep this from recurring across a suite: one declared locator constant per element, unpacked with a star for direct finds and passed whole to conditions.

for a principal

Consider what an ergonomic difference this small does to a mixed-language team's review burden, and whether a thin in-house wrapper is worth the indirection it adds.

## The rule in one line In Selenium's Python client, `find_element` takes the strategy and the value as **two arguments**, while every helper in `selenium.webdriver.support.expected_conditions` takes a **single locator argument**. A single argument holding two values is a tuple, so the pair is written `(By.ID, "term-average")` - and inside a helper call that produces the double parentheses that look like a typo and are not. ```python from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, "term-average")) ) ``` The inner parentheses build the tuple; the outer ones are the call. Remove one pair and the code is wrong in a way the interpreter notices immediately. ## Why the two calls disagree about shape Nothing here is arbitrary once you look at the signatures. - `find_element(by, value)` is a method on the driver, and it was designed to take the two halves positionally. It has room for two parameters, so it uses them. - A condition helper is a **factory that returns a callable** the wait evaluates later. It has to accept a locator as one thing it can carry around, so it takes one parameter, and the natural one-value container for a pair in Python is a tuple. The helper names that take a locator this way follow a visible convention: they end in `_located`, as in `presence_of_element_located`, `visibility_of_element_located` and `invisibility_of_element_located`. When you see `_located`, expect a tuple. ## What getting it wrong looks like Writing `EC.visibility_of_element_located(By.ID, "term-average")` raises a `TypeError` at the point of the call, complaining that the function took one positional argument but two were given. That is a good failure: it happens before the browser is touched and it names the real problem. The mistake is not a subtle timing bug - it is an arity mistake, and the test cannot even start. The reverse mistake is quieter to reason about but just as immediate: passing the tuple to the find call itself, `driver.find_element((By.ID, "term-average"))`, leaves the second parameter unfilled and the first holding something the client cannot use as a strategy. ## Keeping the report-card locators in one place Because the tuple is the natural unit, a Python page module for the report-card viewer usually declares its locators once and uses them both ways: - `TERM_AVERAGE = (By.ID, "term-average")` - the stored pair. - `driver.find_element(*TERM_AVERAGE)` - unpacked back into two arguments for a direct find. - `EC.visibility_of_element_located(TERM_AVERAGE)` - passed whole to a condition helper. That single declaration is the practical reason the tuple convention is worth internalising rather than memorising: it lets the same constant serve both call shapes, with a `*` marking the difference. ## One declaration, two call shapes Put together, a small report-card page module in Python looks like this, and it is the shortest way to see why the tuple convention exists at all: ```python from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait TERM_AVERAGE = (By.ID, "term-average") GRADE_ROWS = (By.CSS_SELECTOR, ".grade-row") def read_term_average(driver): WebDriverWait(driver, 10).until(EC.visibility_of_element_located(TERM_AVERAGE)) return driver.find_element(*TERM_AVERAGE).text ``` Two lines in that snippet carry the whole idea: - `TERM_AVERAGE` is passed **whole** to the condition helper, because the helper wants one locator. - The same constant is passed **unpacked**, with a leading `*`, to `find_element`, because that method wants two arguments. The star is therefore the visible marker of the shape change. When you read Python Selenium code and find a locator constant used with a star on one line and without one on the next, nothing unusual is happening: both lines are using the same declared pair, each in the form its own call requires. ## The same locator in the other bindings | Binding | Direct find | Passed to a wait condition | |---|---|---| | Python | `find_element(By.ID, "term-average")` | `visibility_of_element_located((By.ID, "term-average"))` | | Java | `findElement(By.id("term-average"))` | `visibilityOfElementLocated(By.id("term-average"))` | | JavaScript | `findElement(By.id('term-average'))` | `until.elementLocated(By.id('term-average'))` | | Ruby | `find_element(id: "term-average")` | inside a block, as an ordinary find | Java and JavaScript never face the question because their locator is already a single object, so it fits a one-argument helper without repackaging. Ruby sidesteps it differently: its wait takes a block, and the block simply performs a normal find, so the locator never has to be handed to a helper at all. Python is the binding where the same two values wear two different shapes, and that is the whole of the confusion.

  • Does driver.find_element accept the tuple as well?
    No. It takes the strategy and the value as two separate positional arguments, so a stored tuple has to be unpacked: `driver.find_element(*TERM_AVERAGE)`. Passing the tuple whole leaves the second parameter empty and the first holding something the client cannot use as a strategy.
  • What is the Java equivalent of that tuple?
    The `By` object itself. `By.id("term-average")` already carries both the strategy and the value, so the same value is accepted by `findElement` and by `ExpectedConditions.visibilityOfElementLocated` without repackaging. The Java client simply never splits the pair, so there is nothing to re-join.

saying these in an interview costs you the question

  • Passes the strategy and value as two arguments to the helper
  • Drops the inner parentheses and blames the wait for failing
  • Thinks find_element also wants the tuple form
  • Copies Java's By.id(...) into a Python condition helper
  • Believes a tuple locator performs a different kind of lookup