How to Use Logical and Bitwise AND Operators in Python
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 true
Short-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:
# Safe division: the second condition only runs if denominator is not zero
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 small
Because 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 inactive
Since user is None (falsy), Python skips user.is_active entirely, preventing an AttributeError.
Return value behavior
A common misconception is that and always returns True or False. It actually returns the value that determined the result:
# Returns the first falsy value encountered
print(0 and "hello")
print("" and "world")
print(None and True)
Output:
0
None
# If all values are truthy, returns the last value
print("hello" and "world")
print(1 and 2 and 3)
Output:
world
3
The 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.
Understanding this return behavior lets you write concise default-value patterns:
username = ""
display_name = username and username.strip() or "Anonymous"
print(display_name)
Output:
Anonymous
Since username is an empty string (falsy), and returns it without calling .strip(). Then or sees the falsy value and returns "Anonymous".
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
# AND: 010 = 2
result = a & b
print(result)
Output:
2
Checking 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: True
Extracting 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:
0b110
Permission 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: True
The 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],
})
# Wrong: using 'and' with Series
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],
})
# Correct: using '&' with parentheses around each condition
filtered = df[(df["age"] > 25) & (df["salary"] < 70000)]
print(filtered)
Output:
name age salary
1 Bob 30 60000
3 Diana 28 55000
Due to Python's operator precedence rules, & binds more tightly than comparison operators like > and <. Without parentheses, the expression is parsed incorrectly:
# This does NOT mean (arr > 3) & (arr < 8)
# It actually means arr > (3 & arr) < 8, which causes unexpected results or errors
result = arr[arr > 3 & arr < 8]
Always wrap each condition in parentheses when using & for filtering:
result = arr[(arr > 3) & (arr < 8)]
NumPy 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 30
Common 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
# Logical check: are both positive?
print(x > 0 and y > 0) # True (correct: boolean logic)
print((x > 0) & (y > 0)) # True (happens to work, but semantically wrong)
# The difference shows with non-boolean values
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
2
For 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)] |
The same logical vs. bitwise distinction applies to the OR operators: use or for boolean logic and | for bitwise operations and array/DataFrame filtering.
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?"