Python provides two distinct AND operators that serve fundamentally different purposes: and for boolean logic and & for bitwise operations. They look similar and can sometimes produce the same result, which is exactly why confusing them leads to subtle bugs. This is especially common when working with data analysis libraries like Pandas and NumPy, where using the wrong operator raises confusing errors.
This guide explains how each operator works, what it returns, and when to use one over the other, with practical examples covering conditional logic, bit manipulation, and data filtering.
Understanding the Core Difference
The key distinction is what each operator evaluates:
andevaluates the truth value of entire objects or expressions. It works at the logical level.&compares individual binary bits of integers. It works at the binary level.
| Operator | Type | Operates On | Returns |
|---|---|---|---|
and |
Logical | Truth values of whole objects | First falsy value, or the last value |
& |
Bitwise | Individual bits of integers | Integer result of bit-by-bit comparison |
Using and for Boolean Logic
The and operator is what you use in everyday conditional logic. It evaluates expressions left to right and supports short-circuit evaluation, meaning Python stops as soon as the outcome is determined.
x = 10
y = 5
if x > 5 and y < 10:
print("Both conditions are true")Output:
Both conditions are trueShort-circuit evaluation
Short-circuiting means that if the first operand is falsy, Python immediately returns it without evaluating the second operand. This makes and ideal for guard conditions that protect against errors:
denominator = 0
if denominator != 0 and 100 / denominator > 5:
print("Result is greater than 5")
else:
print("Cannot divide or result too small")Output:
Cannot divide or result too smallBecause denominator != 0 is False, Python never evaluates 100 / denominator, avoiding a ZeroDivisionError.
The same pattern works for safely accessing attributes on objects that might be None:
user = None
if user and user.is_active:
print("User is active")
else:
print("No user or user is inactive")Output:
No user or user is inactiveReturn value behavior
A common misconception is that and always returns True or False. It actually returns the value that determined the result:
print(0 and "hello")
print("" and "world")
print(None and True)Output:
0
Noneprint("hello" and "world")
print(1 and 2 and 3)Output:
world
3The logic is straightforward: if and finds a falsy value, it returns that value immediately (short-circuit). If every value is truthy, it returns the last one because that is the value that completed the evaluation.
Using & for Bitwise Operations
The & operator works at the binary level. It compares each pair of corresponding bits in two integers and produces a 1 only where both bits are 1:
a = 6 # Binary: 110
b = 3 # Binary: 011
result = a & b
print(result)Output:
2Checking if a number is even or odd
The least significant bit of any integer determines whether it is even (bit is 0) or odd (bit is 1). Using & 1 is a fast way to check:
number = 42
is_even = (number & 1) == 0
print(f"{number} is even: {is_even}")
number = 37
is_odd = (number & 1) == 1
print(f"{number} is odd: {is_odd}")Output:
42 is even: True
37 is odd: TrueExtracting bits with a mask
You can isolate specific bits from a value by ANDing it with a bitmask. Only the bits that are 1 in both the value and the mask survive:
value = 0b11010110
mask = 0b00001111 # Keep only the lower 4 bits
lower_bits = value & mask
print(bin(lower_bits))Output:
0b110Permission flags
Bitwise AND is widely used in permission systems where each bit represents a specific capability:
READ_PERMISSION = 0b100 # 4
WRITE_PERMISSION = 0b010 # 2
EXECUTE_PERMISSION = 0b001 # 1
user_permissions = 0b101 # Read and execute, but not write
can_read = bool(user_permissions & READ_PERMISSION)
can_write = bool(user_permissions & WRITE_PERMISSION)
can_execute = bool(user_permissions & EXECUTE_PERMISSION)
print(f"Can read: {can_read}")
print(f"Can write: {can_write}")
print(f"Can execute: {can_execute}")Output:
Can read: True
Can write: False
Can execute: TrueThe Pandas and NumPy Exception
This is where the distinction between and and & becomes critical in practice. When filtering arrays or DataFrames, you must use &, not and. These libraries override the & operator to perform element-wise comparisons across entire arrays or Series.
Why and fails with DataFrames
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana"],
"age": [25, 30, 35, 28],
"salary": [50000, 60000, 75000, 55000],
})
filtered = df[df["age"] > 25 and df["salary"] < 70000]Output:
ValueError: The truth value of a Series is ambiguous.
Use a.empty, a.bool(), a.item(), a.any() or a.all().The error occurs because and tries to evaluate the truth value of the entire Series df["age"] > 25 as a single boolean. A Series containing multiple values cannot be reduced to a single True or False, so Python raises a ValueError.
The correct approach with &
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana"],
"age": [25, 30, 35, 28],
"salary": [50000, 60000, 75000, 55000],
})
filtered = df[(df["age"] > 25) & (df["salary"] < 70000)]
print(filtered)Output:
name age salary
1 Bob 30 60000
3 Diana 28 55000NumPy array filtering
The same rules apply to NumPy arrays:
import numpy as np
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
result = arr[(arr > 3) & (arr < 8)]
print(result)Output:
[4 5 6 7]Combining multiple conditions in Pandas
You can chain as many conditions as needed with &. For readability, place each condition on its own line:
import pandas as pd
df = pd.DataFrame({
"product": ["A", "B", "C", "D", "E"],
"price": [100, 250, 175, 300, 125],
"stock": [50, 0, 30, 10, 0],
})
available_midrange = df[
(df["price"] >= 100)
& (df["price"] <= 200)
& (df["stock"] > 0)
]
print(available_midrange)Output:
product price stock
0 A 100 50
2 C 175 30Common Mistake: Mixing Up and and &
Using & where you meant and in regular Python code can produce results that look correct for some inputs but are silently wrong for others:
x = 3
y = 5
print(x > 0 and y > 0) # True (correct: boolean logic)
print((x > 0) & (y > 0)) # True (happens to work, but semantically wrong)
a = 6 # Binary: 110
b = 3 # Binary: 011
print(a and b) # 3 (returns last truthy value)
print(a & b) # 2 (bitwise AND: 110 & 011 = 010)Output:
True
True
3
2For a and b, both 6 and 3 are truthy, so and returns the last value: 3. For a & b, the bits are compared individually, producing 2. These are completely different operations that happen to both return truthy values here, making the bug easy to miss.
Quick Reference
| Scenario | Operator | Example |
|---|---|---|
if statements |
and |
if x > 0 and y > 0: |
| Guard / safety checks | and |
if obj and obj.valid: |
| Bit manipulation | & |
flags & MASK |
| NumPy array filtering | & |
arr[(arr > 0) & (arr < 10)] |
| Pandas DataFrame filtering | & |
df[(df.a > 1) & (df.b < 5)] |
Summary
Python's two AND operators serve fundamentally different purposes:
- Use
andfor everyday boolean logic inifstatements, guard conditions, and any situation where you are evaluating the truthiness of whole expressions. It short-circuits and returns the actual value that determined the result. - Use
&for bitwise operations on integers, such as checking flags, applying masks, or testing even/odd. It compares individual bits. - In Pandas and NumPy, always use
&(with parentheses around each condition) for element-wise filtering. Usingandwill raise aValueErrorbecause these libraries cannot reduce an entire array or Series to a single boolean value. - When in doubt, remember:
andanswers "are both of these things true?" while&answers "which bits are set in both of these numbers?"