Skip to main content

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:

  • and evaluates 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.
OperatorTypeOperates OnReturns
andLogicalTruth values of whole objectsFirst falsy value, or the last value
&BitwiseIndividual bits of integersInteger 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
note

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.

tip

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
Parentheses are mandatory

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

ScenarioOperatorExample
if statementsandif x > 0 and y > 0:
Guard / safety checksandif 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)]
info

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 and for everyday boolean logic in if statements, 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. Using and will raise a ValueError because these libraries cannot reduce an entire array or Series to a single boolean value.
  • When in doubt, remember: and answers "are both of these things true?" while & answers "which bits are set in both of these numbers?"