You have a Postman collection with 40 requests. Organized into folders. With test scripts that check status codes. You spent time on this. It's good.
You also have a CI pipeline that has never heard of Postman and doesn't plan to.
These two things have coexisted peacefully for months because nobody wants to be the person who manually rewrites 40 requests as pytest functions. There's also Newman — but Newman runs tests, it doesn't generate code you can read, modify, or version properly.
So the collection documents the API. The CI tests the API. They describe the same system and have never met.
I built postman2pytest to introduce them.
One Command
pip install postman2pytest
postman2pytest \
--collection my_api.postman_collection.json \
--out tests/test_api.py
BASE_URL=https://staging.example.com pytest tests/test_api.py -v
The output is plain Python. Readable, editable, committable. No framework lock-in, no runtime wrapper, no magic.
What the Output Looks Like
Given a Postman collection with a Users folder containing POST /api/v1/users with a test script asserting status 201:
def test_users_post_create_user():
"""POST ENV_base_url/api/v1/users (users)"""
url = f"{BASE_URL}/api/v1/users"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ.get('token', '')}",
}
body = json.loads('{"name": "John Doe", "email": "john@example.com"}')
response = requests.post(url, headers=headers, json=body)
assert response.status_code == 201, (
f"Expected 201, got {response.status_code}: {response.text[:200]}"
)
A few things worth noticing:
Folder names end up in the function name. Create user inside Users → test_users_post_create_user. If you have 40 requests and three folders called List, you'll thank this later.
Postman variables become environment variables. {{base_url}} → BASE_URL env var. {{token}} in an Authorization header → os.environ.get('token', '') in an f-string. The generated tests are environment-aware out of the box.
Status codes come from your existing test scripts. If you wrote pm.response.to.have.status(201) in Postman, the generated test asserts exactly 201. No test script → defaults to 200.
Disabled headers stay disabled. You toggled them off in Postman for a reason.
The Architecture
Two stages, cleanly separated.
Parse (core/parser.py) — reads the Postman JSON and produces a flat list of ParsedRequest objects, validated with Pydantic v2. Nested folders are flattened recursively. Malformed items are skipped with a warning; the rest of the collection still generates.
class ParsedRequest(BaseModel):
name: str
method: str
url: str
headers: dict[str, str]
body: str | None
expected_status: int
folder: str | None
Generate (core/generator.py) — takes the flat list and renders a Jinja2 template. The tricky part is variable substitution: {{base_url}}/api/v1/users needs to become f"{BASE_URL}/api/v1/users" in Python, and Bearer {{token}} in a header needs to become f"Bearer {os.environ.get('token', '')}". Two custom Jinja2 filters handle this: strip_base_url for URLs, render_header_value for header values.
The split is deliberate — you can use the parser independently to generate a different output format. The template is the only thing that knows what pytest looks like.
What It Doesn't Do (Yet)
- Postman environments (the
.postman_environment.jsonfile) - OAuth 2.0 flows
- Pre-request scripts
- Response body assertions
These are all solvable. v1.0 is small enough to be trustworthy. I'd rather you use it and tell me what's missing than promise features I haven't built.
36 Tests, Because Eating Your Own Dogfood Matters
pip install postman2pytest pytest
pytest tests/ -v # 36 passed
CI runs on Python 3.10, 3.11, and 3.12 via GitHub Actions.
Why Not Just Use Newman?
Newman runs your Postman tests. That's useful. But it doesn't generate code — it generates a report. When the test fails in CI, Newman tells you it failed. pytest tells you it failed, shows you the diff, lets you add fixtures, parametrize the case, integrate with your existing test infrastructure.
If your team runs pytest for unit tests, integration tests, and contract tests, having your API smoke tests in the same runner means one command, one report, one CI step.
GitHub: github.com/golikovichev/postman2pytest
PyPI: pypi.org/project/postman2pytest
If you hit a collection format this doesn't handle, open an issue.
Top comments (0)