PostgreSQL / MySQL OperationalError: FATAL password authentication failed for user
Verify database credentials, URL-encode special characters in connection strings (urllib.parse.quote_plus), and inspect pg_hba.conf authentication rules.
Root Cause Analysis
This error occurs when Python tries to connect to a relational database server (such as PostgreSQL or MySQL) using psycopg, pymysql, or SQLAlchemy, but the database server rejects the connection handshake due to invalid credentials, incorrect password encryption, or restrictive host access rules.
1. Unescaped Special Characters in Database URIs
In SQLAlchemy connection strings (e.g., postgresql://user:p@ss#word@localhost:5432/db), special characters like @, #, :, or / in passwords corrupt URI parsing. SQLAlchemy treats the @ inside the password as the boundary between credentials and host, sending a truncated password.
2. Environment Variable Configuration Drift
In containerized deployments, mismatch between .env file credentials and initialized database container volume credentials causes immediate authentication rejection.
3. PostgreSQL pg_hba.conf Authentication Methods (scram-sha-256 vs md5)
PostgreSQL 14+ enforces scram-sha-256 password hashing. Connecting with older client drivers or mismatched user password formats triggers fatal authentication failures.
4. Default Database and User Privileges
Attempting to authenticate as postgres or root from remote IP addresses when the database configuration only allows local socket connections.
Reproduction Code (MCVE)
# **Note de reproductibilité :** Dépendant de la configuration du serveur SQL distant.
class OperationalError(Exception):
pass
raise OperationalError('FATAL: password authentication failed for user "app_user" (server rejected credentials)')
Solution 1: URL-Encode Special Characters in Connection Strings
Use urllib.parse.quote_plus to safely escape special characters in passwords when constructing database URLs.
import urllib.parse
user = 'db_admin'
password = 'p@ss#word/2026!'
host = 'localhost'
port = 5432
database = 'production_db'
# Safely encode password
encoded_pwd = urllib.parse.quote_plus(password)
db_uri = f'postgresql+psycopg2://{user}:{encoded_pwd}@{host}:{port}/{database}'
print(f'Safely encoded database URI: {db_uri}')
Solution 2: Use Environment Variables with Structured Connection Dict
Pass credentials as a structured dictionary rather than a raw string to avoid URL parsing issues entirely.
import os
connection_params = {
'user': os.getenv('DB_USER', 'postgres'),
'password': os.getenv('DB_PASSWORD', 'secret123'),
'host': os.getenv('DB_HOST', '127.0.0.1'),
'port': int(os.getenv('DB_PORT', 5432)),
'dbname': os.getenv('DB_NAME', 'app_db')
}
print(f'Ready to connect to host: {connection_params["host"]} with user {connection_params["user"]}')
A common mistake is storing unquoted passwords containing % in .env files. Python dotenv parsers may interpolate % as variable expansions. Always wrap values in single quotes inside .env files. Edge cases occur with Docker Compose: changing POSTGRES_PASSWORD in docker-compose.yml does NOT update an existing PostgreSQL data volume in pgdata/. You must delete the volume (docker-compose down -v) to reinitialize credentials. Contrast this error with OperationalError: could not connect to server, which indicates network or port unreachability.