Fix psycopg2 "IndexError: tuple index out of range" when using LIKE with parameterized queries. Use when: (1) cursor.execute() fails with IndexError on a query containing LIKE '%pattern%', (2) SQL LIKE wildcards conflict with psycopg2 %s parameter placeholders, (3) Query works in psql but fails in Python. The % character has dual meaning: SQL LIKE wildcard AND psycopg2's parameter substitution marker.
Scanned 5/27/2026
Install via CLI
openskills install divinevideo/divine-mobile---
name: psycopg2-like-percent-escape
description: |
Fix psycopg2 "IndexError: tuple index out of range" when using LIKE with parameterized queries.
Use when: (1) cursor.execute() fails with IndexError on a query containing LIKE '%pattern%',
(2) SQL LIKE wildcards conflict with psycopg2 %s parameter placeholders, (3) Query works
in psql but fails in Python. The % character has dual meaning: SQL LIKE wildcard AND
psycopg2's parameter substitution marker.
author: Claude Code
version: 1.0.0
date: 2026-01-29
---
# psycopg2 LIKE Clause Percent Sign Escaping
## Problem
When using psycopg2 with parameterized queries containing SQL LIKE patterns, the `%` character
causes conflicts. The `%` is used both as:
1. SQL LIKE wildcard (e.g., `'%pattern%'`)
2. psycopg2's parameter placeholder marker (e.g., `%s`)
This results in confusing errors like `IndexError: tuple index out of range` because psycopg2
interprets `%c` in `%cdn` as a format specifier.
## Context / Trigger Conditions
- `IndexError: tuple index out of range` from `cursor.execute()`
- Query contains hardcoded LIKE pattern: `WHERE column LIKE '%something%'`
- Query also uses `%s` parameters for other values
- Query works in psql or pgAdmin but fails in Python
Example failing code:
```python
cursor.execute("""
SELECT * FROM vines
WHERE url LIKE '%cdn.vine.co%'
LIMIT %s OFFSET %s
""", (1000, 0))
# IndexError: tuple index out of range
```
## Solution
### Option 1: Escape `%` with `%%` (for static patterns)
Double the percent signs in hardcoded LIKE patterns:
```python
cursor.execute("""
SELECT * FROM vines
WHERE url LIKE '%%cdn.vine.co%%'
LIMIT %s OFFSET %s
""", (1000, 0))
```
### Option 2: Pass LIKE pattern as parameter (recommended)
The cleaner approach - pass the entire LIKE pattern as a parameter:
```python
pattern = '%cdn.vine.co%'
cursor.execute("""
SELECT * FROM vines
WHERE url LIKE %s
LIMIT %s OFFSET %s
""", (pattern, 1000, 0))
```
This is the recommended approach because:
- No escaping confusion
- Pattern can be dynamically constructed
- Follows parameterized query best practices
### Option 3: Use psycopg2.sql module for complex cases
For dynamic SQL construction:
```python
from psycopg2 import sql
query = sql.SQL("""
SELECT * FROM {table}
WHERE url LIKE %s
""").format(table=sql.Identifier('vines'))
cursor.execute(query, ('%cdn.vine.co%',))
```
## Verification
After applying the fix:
1. Query executes without IndexError
2. Results correctly match the LIKE pattern
3. Other `%s` parameters are still substituted correctly
## Example
Before (broken):
```python
def get_vines_by_cdn(db, limit, offset):
cursor = db.cursor()
cursor.execute("""
SELECT vine_id, url FROM discovered_vines
WHERE url LIKE '%cdn.vine.co%'
ORDER BY created_at
LIMIT %s OFFSET %s
""", (limit, offset))
return cursor.fetchall()
```
After (fixed with Option 1):
```python
def get_vines_by_cdn(db, limit, offset):
cursor = db.cursor()
cursor.execute("""
SELECT vine_id, url FROM discovered_vines
WHERE url LIKE '%%cdn.vine.co%%'
ORDER BY created_at
LIMIT %s OFFSET %s
""", (limit, offset))
return cursor.fetchall()
```
After (fixed with Option 2 - recommended):
```python
def get_vines_by_cdn(db, limit, offset):
cursor = db.cursor()
cdn_pattern = '%cdn.vine.co%'
cursor.execute("""
SELECT vine_id, url FROM discovered_vines
WHERE url LIKE %s
ORDER BY created_at
LIMIT %s OFFSET %s
""", (cdn_pattern, limit, offset))
return cursor.fetchall()
```
## Notes
- This issue only affects parameterized queries with `%s` placeholders
- Raw SQL strings without parameters don't have this problem
- The `%%` escape only works when the query uses psycopg2's parameter substitution
- Django's ORM handles this automatically; this is a raw SQL issue
- psycopg3 uses `$1, $2` style placeholders, avoiding this conflict entirely
## Related Issues
- Searching for literal `%` in data requires additional escaping with `ESCAPE` clause
- Similar issues can occur with `_` (single character wildcard) if using `%_` pattern
## References
- [psycopg2 Basic Module Usage](https://www.psycopg.org/docs/usage.html)
- [psycopg2 sql Module Documentation](https://www.psycopg.org/docs/sql.html)
- [PostgreSQL Pattern Matching](https://www.postgresql.org/docs/current/functions-matching.html)
- [psycopg2 Issue #825 - Percent sign escaping](https://github.com/psycopg/psycopg2/issues/825)
No comments yet. Be the first to comment!