Skip to content
Back to skills

Psycopg2 Like Percent Escape

ASecurity

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.

  • 265 stars
  • 0 votes
  • 0 copies
  • 8 views
  • Added May 27, 2026
developmentpythongosqldjangogitapidocumentation

Works with

  • claude code
  • cursor
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add divinevideo/divine-mobile --skill psycopg2-like-percent-escape --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Psycopg2 Like Percent Escape?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Psycopg2 Like Percent Escape
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/divinevideo-psycopg2-like-percent-escape/badge)](https://www.skillsdirectory.com/skills/divinevideo-psycopg2-like-percent-escape)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.