Virtual Environments and requirements.txt for Appium Projects

5 min read

Python's global package space is a shared resource. Installing test dependencies globally creates version conflicts when different projects need different versions of the same library. Virtual environments solve this: each project gets its own isolated Python environment.

Creating a virtual environment

# Create a venv in the project directory
python3 -m venv .venv
 
# Activate it
source .venv/bin/activate          # macOS / Linux
.venv\Scripts\activate             # Windows
 
# Confirm activation — prompt changes to (.venv)
which python  # should show .venv/bin/python

Always activate before running any pip installs or test commands. If the prompt shows the venv name, it's active.

Installing dependencies

pip install Appium-Python-Client pytest pytest-html allure-pytest

Pinning with requirements.txt

requirements.txt captures the exact versions for reproducible installs:

pip freeze > requirements.txt

A typical requirements.txt for an Appium suite:

Appium-Python-Client==3.1.0
selenium==4.18.1
pytest==8.1.0
pytest-html==4.1.1
allure-pytest==2.13.2
pytest-xdist==3.5.0
python-dotenv==1.0.1

For teammates and CI:

pip install -r requirements.txt

requirements.txt vs pyproject.toml

requirements.txt is the simplest approach for test-only projects. pyproject.toml with a tool like poetry or hatch is better for projects that also publish packages, but adds complexity. For a test suite with no publication needs, stick with requirements.txt.

Project directory layout

appium-python-suite/
├── .venv/                    # never commit this
├── requirements.txt
├── pytest.ini                # or pyproject.toml [tool.pytest.ini_options]
├── conftest.py               # shared fixtures
├── pages/
│   ├── __init__.py
│   ├── base_page.py
│   ├── login_page.py
│   └── home_page.py
├── tests/
│   ├── __init__.py
│   ├── test_login.py
│   └── test_checkout.py
├── utils/
│   ├── __init__.py
│   ├── gesture_utils.py
│   └── wait_utils.py
└── apps/
    ├── app-debug.apk
    └── MyApp.app

.gitignore entries

.venv/
__pycache__/
*.pyc
.pytest_cache/
allure-results/
screenshots/
*.log
.env

Never commit .venv/ — it contains platform-specific binaries and is rebuilt from requirements.txt.

pytest.ini

Configure pytest defaults in pytest.ini:

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = --tb=short -v

addopts = --tb=short -v applies to every run. -v shows test names; --tb=short shows short tracebacks (readable without being overwhelming).

Environment variables with python-dotenv

Device names, app paths, and Appium server URLs should come from environment variables, not hardcoded in test code:

.env file (gitignored):

APPIUM_SERVER=http://127.0.0.1:4723
ANDROID_DEVICE=emulator-5554
IOS_DEVICE=iPhone 15
APP_PATH_ANDROID=apps/app-debug.apk
APP_PATH_IOS=apps/MyApp.app

Load in conftest.py:

from dotenv import load_dotenv
import os
 
load_dotenv()
 
APPIUM_SERVER = os.getenv("APPIUM_SERVER", "http://127.0.0.1:4723")
ANDROID_DEVICE = os.getenv("ANDROID_DEVICE", "emulator-5554")

CI systems (GitHub Actions, GitLab CI) set environment variables natively — load_dotenv() falls back to actual environment variables when no .env file is present, so the same code works in both contexts.

// tip to track lessons you complete and pick up where you left off across devices.