Добавить в корзинуПозвонить
Найти в Дзене
Аналитика данных

Python. Переменные окружения на корпоративном сервере: 3 приёма, которые спасут часы отладки

Работаешь на удалённом рабочем столе под Windows. Подключаешься такой к БД с помощью секретов из .env-файл. Но получаешь странные ошибки вроде Authentication failed или Connection refused, хотя логин и пароль точно верные. Скорее всего, проблема не в базе данных и не в сети. Проблема — в том, как именно Python читает .env-файл на корпоративном сервере. Разберём три неочевидных приёма, которые заставят код работать. Все три приёма я собрал из собственного опыта работы с корпоративными серверами Windows, где каждая мелочь может стать причиной часов отладки. Подключился по RDP к корпоративному серверу, открыл Jupyter Notebook, написал код: from dotenv import load_dotenv
import os
import psycopg2 load_dotenv('.env') conn = psycopg2.connect(host=os.getenv('HOST'), port=os.getenv('PORT'), user=os.getenv('USERNAME'), password=os.getenv('PASSWORD'), dbname=os.getenv('DATABASE')) После запуска консоль выдаёт что-то типа: OperationalError: FATAL: password authentication failed for user "Corp_Nam
Оглавление

Работаешь на удалённом рабочем столе под Windows. Подключаешься такой к БД с помощью секретов из .env-файл. Но получаешь странные ошибки вроде Authentication failed или Connection refused, хотя логин и пароль точно верные.

Скорее всего, проблема не в базе данных и не в сети. Проблема — в том, как именно Python читает .env-файл на корпоративном сервере.

Разберём три неочевидных приёма, которые заставят код работать. Все три приёма я собрал из собственного опыта работы с корпоративными серверами Windows, где каждая мелочь может стать причиной часов отладки.

Ситуация

Подключился по RDP к корпоративному серверу, открыл Jupyter Notebook, написал код:

from dotenv import load_dotenv
import os
import psycopg2
load_dotenv('.env')
conn = psycopg2.connect(host=os.getenv('HOST'), port=os.getenv('PORT'), user=os.getenv('USERNAME'), password=os.getenv('PASSWORD'), dbname=os.getenv('DATABASE'))

После запуска консоль выдаёт что-то типа:

OperationalError: FATAL: password authentication failed for user "Corp_Name_Surname"

Проверяеim файл .env — логин и пароль верные. Пробуешь подключиться через DBeaver — всё работает. Через psql в командной строке — тоже работает. А в Python — нет. Что делать?

Три скрытые проблемы .env в корпоративной среде

Проблема №1. Окружение Windows

В Windows есть системные переменные окружения, которые устанавливаются автоматически. Одна из них — USERNAME. Она содержит имя текущего пользователя Windows (например, Corp_Name_Surname).

Когда вызывается load_dotenv('.env') без дополнительных параметров, библиотека python-dotenv по умолчанию не перезаписывает уже существующие переменные окружения. Это значит:

Если в .env:

USERNAME = analytic_user
PASSWORD = SuperSecret123
HOST = some-leads.pg.garant
load_dotenv('.env')

Но:

os.getenv('USERNAME') # → 'Corp_Name_Surname' ❌ НЕ логин из .env
os.getenv('PASSWORD') # → 'SuperSecret123' ✅ Пароль из .env

В результате Python пытается подключиться к БД под пользователем Windows, а не под учёткой БД. Ошибка аутентификации гарантирована.

Проблема №2. Скрытые пробелы и переносы строк

Когда вы редактируете .env-файл в Блокноте на удалённом сервере, легко не заметить:

  • Пробел в конце строки: PASSWORD=MyPass123 (невидимый пробел после 3)
  • Символ возврата каретки \r от Windows (CRLF vs LF)
  • Перенос строки, который «прилип» к значению

В резултате база данных получает пароль 'MyPass123<пробел>' вместо 'MyPass123' — и, конечно, отказывает в доступе.

Проблема №3. Спецсимволы в пароле ломают URL подключения

Если вы формируете строку подключения вручную (для SQLAlchemy, requests к ClickHouse и т.п.):

conn_str = f"postgresql+psycopg2://{USERNAME}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}"

Если пароль содержит спецсимволы: P@ssw0rd#2026!

То символы @, #, !, :, / имеют специальное значение в URL. Пароль P@ssw0rd#2026! превратится в:

postgresql+psycopg2://user:P@ssw0rd#2026!@host:5432/db

Парсер URL «увидит» @ внутри пароля и решит, что это разделитель между учёткой и хостом. Результат — Connection refused или Invalid URL.

Решение. Три приёма в одном блоке кода

Вот финальный код, который решает все три проблемы сразу:

import os
from dotenv import load_dotenv
from urllib.parse import quote_plus

Перезаписываем системные переменные Windows: override=True

load_dotenv('.env', override=True)

Убираем скрытые пробелы и переносы строк: strip()

USERNAME = os.getenv('USERNAME').strip()
PASSWORD = os.getenv('PASSWORD').strip()
HOST = os.getenv('HOST').strip()
DATABASE = os.getenv('DATABASE').strip()
PORT = os.getenv('PORT').strip()

Кодируем спецсимволы из пароля для URL: quote_plus()

safe_password = quote_plus(PASSWORD)
# Проверка: убеждаемся, что всё считалось корректно
print(f"USERNAME: {USERNAME}")
print(f"PASSWORD: {PASSWORD}")
print(f"HOST: {HOST}")
print(f"DATABASE: {DATABASE}")
print(f"PORT: {PORT}")
# Пример безопасного URL для SQLAlchemy
conn_str = f"postgresql+psycopg2://{USERNAME}:{safe_password}@{HOST}:{PORT}/{DATABASE}"

Важные нюансы

quote_plus()

Кодирует спецсимволы в «URL-безопасный» вид, заменяя их на %XX-последовательности:

  • Было: P@ssw0rd — стало: P%40ssw0rd
  • Было: My#Pass!2026 — стало: My%23Pass%212026
  • Было: user:pass/word — стало: user%3Apass%2Fword
  • Было: пароль с пробелом — стало: пароль+%D1%81+пробелом

Что кодируется, а что нет

  • quote_plus кодирует всё, кроме:
  • Букв (a-z, A-Z)
  • Цифр (0-9)
  • Символов _, ., -, ~
  • Всё остальное (@, #, !, :, /, ?, &, =, пробелы, кириллица) превращается в %XX.

override=True

Перезаписывает все переменные, которые есть в .env. Это значит:

  • USERNAME из .env победит системный USERNAME Windows
  • Но если в .env случайно окажется PATH или TEMP — они тоже перезапишутся

Поэтому в .env нужно хранить только то, что действительно нужно для проекта. Не копируй туда системные переменные.

Заключение

Три строчки кода, которые экономят часы отладки:

  • load_dotenv('.env', override=True) # против системных переменных
  • PASSWORD = os.getenv('PASSWORD').strip() # против скрытых пробелов
  • safe_password = quote_plus(PASSWORD) # против спецсимволов в URL