Python Best Practices for .NET Engineers¶
1. Introduction¶
Python and .NET are often presented as competing ecosystems, but in enterprise engineering they are usually complementary. For a senior .NET engineer, the most productive framing is not "How do I rewrite C# in Python?" but "Which parts of the problem are better served by Python, and how do I keep the same engineering discipline?"
Python alongside .NET — when and why¶
Python fits particularly well beside .NET when you need:
- ML and data tooling where the ecosystem is stronger in Python, including scikit-learn, pandas, NumPy, and experiment-heavy workflows.
- Automation and orchestration for build tooling, operational scripts, ETL pipelines, and internal developer utilities.
- API backends with fast iteration where FastAPI, Pydantic, and Python's concise syntax reduce delivery time.
- Glue code between systems when the main need is integration rather than CPU-bound throughput.
- Research-to-production transitions where data scientists already prototype in Python and engineering needs to productionize the result.
In many enterprise environments, a practical split looks like this:
- .NET for core business platforms, high-throughput service estates, internal platforms, and long-lived domain-heavy systems.
- Python for ML services, data workflows, lightweight APIs, and automation-heavy workloads.
Key mindset differences from C¶
The biggest adjustment is not syntax. It is how much structure is enforced by the language versus by team conventions.
| Topic | C# mindset | Python mindset |
|---|---|---|
| Language guidance | Compiler and framework enforce many patterns | Teams enforce patterns through conventions, tooling, and reviews |
| Typing | Static typing is central | Typing is optional at runtime but essential in serious codebases |
| Architecture | Heavier framework scaffolding is common | Simpler module boundaries are common |
| Encapsulation | Access modifiers and interfaces are primary tools | Naming conventions, protocols, and module design are more common |
| Async | Task-based async is deeply standardized |
asyncio is powerful but easier to misuse if conventions are weak |
| Dependency injection | DI container is standard in ASP.NET Core | DI exists, but Python often mixes container DI with framework-provided injection |
When moving from C# to Python, keep these principles in mind:
- Prefer clarity over abstraction density.
- Use type hints, tests, and linting to replace some of the safety you are used to getting from the compiler.
- Keep architecture explicit. Python lets you do almost anything; that is precisely why boundaries matter more.
- Avoid recreating every .NET pattern mechanically. Translate the intent of the pattern, not the syntax.
2. Project Structure¶
Enterprise FastAPI application layout¶
A maintainable Python service benefits from a package-based layout that keeps transport, domain, infrastructure, and configuration concerns separate.
project-root/
├── pyproject.toml
├── README.md
├── .env
├── alembic.ini
├── migrations/
│ └── versions/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── logging.py
│ │ └── security.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── dependencies.py
│ │ └── v1/
│ │ ├── __init__.py
│ │ ├── router.py
│ │ └── endpoints/
│ │ ├── __init__.py
│ │ ├── health.py
│ │ └── users.py
│ ├── domain/
│ │ ├── __init__.py
│ │ ├── entities/
│ │ ├── services/
│ │ └── repositories/
│ ├── application/
│ │ ├── __init__.py
│ │ ├── dtos/
│ │ ├── use_cases/
│ │ └── interfaces/
│ ├── infrastructure/
│ │ ├── __init__.py
│ │ ├── db/
│ │ ├── messaging/
│ │ └── repositories/
│ └── schemas/
│ ├── __init__.py
│ ├── requests/
│ └── responses/
├── tests/
│ ├── conftest.py
│ ├── unit/
│ ├── integration/
│ └── api/
└── scripts/
├── seed_data.py
└── smoke_test.py
How this maps to a .NET solution¶
A senior .NET engineer can think of the structure this way:
app/api/is similar to ASP.NET Core controllers or minimal API endpoints.app/application/is similar to an application layer containing handlers, DTOs, and orchestration logic.app/domain/is similar to your domain project with entities, business rules, and abstractions.app/infrastructure/is similar to EF Core, external clients, message bus adapters, and repository implementations.app/core/is where Python projects often place cross-cutting concerns such as config, logging, and auth.schemas/usually corresponds to request/response contracts, often combining the role that DTOs and validation attributes play in C#.
A key difference is that Python often keeps this in one package tree rather than multiple .csproj files. You can still preserve the same architecture; the boundaries are organizational rather than assembly-based.
__init__.py conventions¶
__init__.py marks a directory as a Python package and can also define what the package exports.
Recommended conventions:
- Keep most
__init__.pyfiles minimal or empty. - Use them to expose a small, intentional public API where helpful.
- Do not put substantial business logic in
__init__.py. - Avoid creating import chains that trigger hidden side effects at import time.
Example:
# app/domain/services/__init__.py
from .user_service import UserService
from .billing_service import BillingService
__all__ = ["UserService", "BillingService"]
C# parallel:
- Think of
__init__.pyas a lightweight mix of namespace organization and public surface shaping. - Unlike C#, importing a package can execute code immediately, so keep imports predictable.
Separation of concerns in Python vs .NET¶
The architectural principle is the same in both ecosystems: isolate transport, business rules, persistence, and infrastructure.
The difference is where the enforcement comes from:
- In .NET, assemblies, interfaces, analyzers, and framework conventions make layer boundaries more explicit.
- In Python, boundaries are maintained through package layout, import discipline, tests, and code review.
Good Python separation of concerns means:
- API routes should validate and delegate, not implement business workflows.
- Domain services should not know about FastAPI request objects.
- Infrastructure should implement abstractions defined closer to the application/domain layers.
- Configuration, secrets, and environment wiring should stay out of domain code.
3. Dependency Injection in Python¶
How Python DI compares to .NET Core DI¶
If you are used to ASP.NET Core, DI is one of the biggest conceptual anchors when learning Python.
In .NET, DI is a default architectural primitive:
- Register services in
IServiceCollection - Inject through constructors
- Use scoped/singleton/transient lifetimes
- Let the framework manage resolution
Python supports the same architectural outcome, but there is no single universal standard. You will commonly see three approaches:
- Manual wiring for small apps.
- Container-based DI using a library such as
dependency-injector. - Framework-level injection in FastAPI using
Depends().
For enterprise services, container-based DI plus FastAPI integration is often the cleanest equivalent to ASP.NET Core.
Implementation using dependency-injector¶
Example container:
from dependency_injector import containers, providers
from app.core.config import Settings
from app.infrastructure.db.session import SessionFactory
from app.infrastructure.repositories.user_repository import UserRepository
from app.domain.services.user_service import UserService
class Container(containers.DeclarativeContainer):
config = providers.Singleton(Settings)
session_factory = providers.Singleton(
SessionFactory,
connection_string=config.provided.database_url,
)
user_repository = providers.Factory(
UserRepository,
session_factory=session_factory,
)
user_service = providers.Factory(
UserService,
user_repository=user_repository,
)
Typical usage in app startup:
from fastapi import FastAPI
from app.container import Container
def create_app() -> FastAPI:
container = Container()
app = FastAPI(title="Engineering Playbook API")
app.container = container
return app
How to interpret this as a .NET engineer:
providers.Singleton(...)is roughly analogous toAddSingleton(...).providers.Factory(...)is closest toAddTransient(...).- You will typically model request-scoped behavior more deliberately in Python because request scope is not as uniformly built into all libraries as it is in ASP.NET Core.
FastAPI built-in DI with Depends()¶
FastAPI has a lightweight DI mechanism for endpoint dependencies.
from fastapi import APIRouter, Depends
from app.domain.services.user_service import UserService
from app.api.dependencies import get_user_service
router = APIRouter()
@router.get("/users/{user_id}")
async def get_user(
user_id: str,
user_service: UserService = Depends(get_user_service),
):
return await user_service.get_user(user_id)
Dependency function:
from dependency_injector.wiring import Provide, inject
from fastapi import Depends
from app.container import Container
from app.domain.services.user_service import UserService
@inject
def get_user_service(
service: UserService = Depends(Provide[Container.user_service]),
) -> UserService:
return service
Think of Depends() as a framework-friendly equivalent to parameter injection for request handlers. It is not a full replacement for good application composition, but it works very well at the API boundary.
Side-by-side comparison with C# constructor injection¶
C¶
public interface IUserService
{
Task<UserDto> GetUserAsync(Guid userId);
}
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet("users/{userId:guid}")]
public async Task<ActionResult<UserDto>> GetUser(Guid userId)
{
var user = await _userService.GetUserAsync(userId);
return Ok(user);
}
}
Python¶
@router.get("/users/{user_id}")
async def get_user(
user_id: str,
user_service: UserService = Depends(get_user_service),
):
return await user_service.get_user(user_id)
Key differences:
- C# emphasizes constructor injection into classes.
- FastAPI commonly injects dependencies directly into endpoint function parameters.
- Python applications can still use constructor injection inside service classes, especially when using
dependency-injector.
A good rule: use container DI for composition, and use FastAPI Depends() for request-bound delivery into handlers.
4. Async Python¶
asyncio fundamentals¶
Python async programming is based on cooperative multitasking using an event loop. The model is conceptually similar to C# async/await, but the runtime ergonomics differ.
Core concepts:
async defdefines a coroutine function.awaityields control until an awaitable completes.- The event loop schedules coroutines.
- Async is best for I/O-bound concurrency, not CPU-bound parallelism.
C# parallel:
async defis analogous toasync Taskmethods.awaitbehaves similarly in spirit to C#await.asynciois the closest conceptual equivalent to theTask-based asynchronous model plus runtime scheduler.
async def and await syntax¶
import httpx
async def fetch_user(user_id: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(f"https://service/users/{user_id}")
response.raise_for_status()
return response.json()
For a .NET engineer, the biggest habit change is that Python distinguishes more sharply between:
- a normal function,
- a coroutine function, and
- the coroutine object produced when you call an async function.
If you forget await, you often end up passing coroutine objects around rather than results.
asyncio.gather() vs Task.WhenAll in C¶
These are close conceptual matches.
Python¶
results = await asyncio.gather(
fetch_user("u1"),
fetch_user("u2"),
fetch_user("u3"),
)
C¶
var results = await Task.WhenAll(
FetchUserAsync("u1"),
FetchUserAsync("u2"),
FetchUserAsync("u3"));
Shared purpose:
- Start multiple asynchronous I/O operations.
- Await all of them together.
- Improve throughput where operations are independent.
Key practical note:
- In Python, be very deliberate about exception handling, cancellation behavior, and whether you want
return_exceptions=Trueinasyncio.gather().
Common async pitfalls in Python¶
These are the async mistakes .NET engineers most often make when they first move into Python:
- Forgetting to await a coroutine
-
Equivalent to creating a
Taskand never awaiting it, but often easier to do accidentally. -
Calling blocking I/O inside async code
- Example: using
requestsinsideasync definstead ofhttpx.AsyncClient. -
This is similar to blocking on synchronous I/O inside an ASP.NET Core async flow.
-
Using async for CPU-bound work
- Async does not make CPU-heavy work faster.
-
Use process pools, task queues, or separate worker services.
-
Creating too many concurrent tasks
- Unbounded
gather()across thousands of items can overload databases and downstream services. -
Apply semaphores, batching, or queue-based concurrency controls.
-
Mixing sync and async dependencies inconsistently
-
Keep call chains consistently async when the underlying work is I/O-bound.
-
Improper client lifetime management
- Recreating DB or HTTP clients per call can hurt performance.
-
Reuse long-lived clients where the library supports it.
-
Confusing thread safety with async safety
- Shared mutable state is still dangerous, even when code is single-threaded most of the time.
Side-by-side comparison table with C¶
| Concern | C# | Python |
|---|---|---|
| Async function declaration | async Task<T> |
async def ... -> T |
| Awaiting work | await SomeAsync() |
await some_async() |
| Aggregate concurrency | Task.WhenAll(...) |
asyncio.gather(...) |
| Async streams | IAsyncEnumerable<T> |
async iterators / async for |
| Common web runtime | ASP.NET Core | FastAPI / Starlette / aiohttp |
| Hidden trap | .Result / .Wait() deadlocks or blocking |
calling sync libraries in async code |
| CPU-bound parallelism | Parallel.ForEach, background workers, hosted services |
multiprocessing, Celery, task queues, worker processes |
5. Type Hints and Pydantic¶
Why type hints matter in enterprise Python¶
Type hints are optional to the interpreter, but they should be treated as mandatory in enterprise Python.
They help with:
- IDE navigation and refactoring
- Static analysis with tools like
mypyorpyright - API clarity for maintainers
- Better framework integration, especially in FastAPI and Pydantic
- Reducing ambiguity in larger teams
For a .NET engineer, type hints are part of what makes Python feel safe enough for long-lived production systems. They do not provide the same guarantees as the C# compiler, but they significantly improve maintainability.
Example:
from collections.abc import Iterable
def calculate_total(prices: Iterable[float], tax_rate: float) -> float:
subtotal = sum(prices)
return subtotal * (1 + tax_rate)
Pydantic models vs C# record types¶
Pydantic models are frequently used for:
- request validation
- response serialization
- config binding
- schema generation
They are not identical to C# records, but records are the closest mental model for many use cases.
Pydantic¶
from pydantic import BaseModel, EmailStr, Field
class CreateUserRequest(BaseModel):
email: EmailStr
display_name: str = Field(min_length=2, max_length=100)
is_active: bool = True
C# record¶
public record CreateUserRequest(
[property: EmailAddress] string Email,
[property: StringLength(100, MinimumLength = 2)] string DisplayName,
bool IsActive = true);
Main comparison:
- Pydantic models combine data shape, validation, parsing, and serialization.
- C# records focus on immutable data representation and typically rely on external validation attributes or FluentValidation.
Validation patterns¶
Recommended validation strategy in Python:
- Use Pydantic field constraints for structural validation.
- Use model validators for cross-field rules.
- Keep business invariants that belong to the domain outside the transport contract when appropriate.
Example:
from pydantic import BaseModel, Field, model_validator
class DateRangeRequest(BaseModel):
start_day: int = Field(ge=1, le=31)
end_day: int = Field(ge=1, le=31)
@model_validator(mode="after")
def validate_range(self) -> "DateRangeRequest":
if self.end_day < self.start_day:
raise ValueError("end_day must be greater than or equal to start_day")
return self
C# parallel:
- Pydantic field validators are similar to a combination of data annotations, custom model binders, and FluentValidation rules.
- Keep transport validation near the API boundary and deeper business rules in the application/domain layers.
Settings management with Pydantic BaseSettings¶
In enterprise Python, configuration should be strongly modeled instead of scattered through os.environ lookups.
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "engineering-playbook"
environment: str = "dev"
database_url: str
redis_url: str | None = None
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="APP_",
extra="ignore",
)
This is very similar in intent to:
.NEToptions binding fromappsettings.json, environment variables, and secret storesIOptions<T>/IOptionsSnapshot<T>for strongly typed configuration
Recommended practice:
- Bind configuration once into a settings object.
- Inject settings rather than reading environment variables in random modules.
- Separate environment-specific deployment configuration from application code.
6. Testing in Python¶
pytest project structure¶
pytest is the standard testing tool in modern Python and maps well to how a .NET engineer thinks about xUnit.
Recommended structure:
tests/
├── conftest.py
├── unit/
│ ├── test_user_service.py
│ └── test_pricing_rules.py
├── integration/
│ ├── test_postgres_user_repository.py
│ └── test_redis_cache.py
└── api/
├── test_health_endpoint.py
└── test_users_endpoint.py
Guidance:
- Put shared fixtures in
conftest.py. - Keep unit, integration, and API tests separate.
- Name tests to reflect behavior.
- Prefer explicit fixture use over opaque test inheritance patterns.
Fixtures vs xUnit constructors¶
This is one of the most useful mental mappings for .NET engineers.
xUnit style thinking¶
In C#, you often initialize shared test state through:
- constructor setup
IClassFixture<T>CollectionFixture
pytest fixture equivalent¶
import pytest
from app.domain.services.user_service import UserService
@pytest.fixture
def user_service(fake_user_repository):
return UserService(user_repository=fake_user_repository)
def test_get_user_returns_expected_user(user_service):
result = user_service.get_user("u1")
assert result.id == "u1"
How to think about it:
- A
pytestfixture is like a more flexible combination of constructor setup, factory helpers, and dependency injection for tests. - Fixtures can be function-, module-, or session-scoped.
- Tests declare what they need in parameters rather than pulling everything from class state.
Mocking with unittest.mock vs Moq in C¶
Python's built-in unittest.mock covers most common mocking scenarios.
Python¶
from unittest.mock import AsyncMock
async def test_service_calls_repository():
repository = AsyncMock()
repository.get_by_id.return_value = {"id": "u1"}
service = UserService(user_repository=repository)
result = await service.get_user("u1")
assert result["id"] == "u1"
repository.get_by_id.assert_awaited_once_with("u1")
C# with Moq¶
var repo = new Mock<IUserRepository>();
repo.Setup(x => x.GetByIdAsync("u1"))
.ReturnsAsync(new User { Id = "u1" });
Key differences:
unittest.mockis more dynamic and less interface-driven.- Python tests often mock by shape and behavior rather than by explicit interface types.
- Autospeccing and disciplined fixture design are important to avoid brittle tests.
Async test patterns¶
When testing async Python code, use pytest with async support such as pytest-asyncio.
import pytest
@pytest.mark.asyncio
async def test_fetch_user_returns_payload(async_client):
response = await async_client.get("/users/u1")
assert response.status_code == 200
Recommended patterns:
- Use async test functions for async code paths.
- Prefer real serialization and validation in API tests.
- Mock network boundaries, not internal implementation details.
- Keep unit tests fast and deterministic.
7. ML Engineering Basics¶
Scikit-learn pipeline structure¶
For teams coming from standard backend engineering, the key ML lesson is to make preprocessing and modeling explicit and versioned.
A basic scikit-learn pipeline might look like this:
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.ensemble import RandomForestClassifier
numeric_features = ["age", "account_balance"]
category_features = ["segment", "region"]
preprocessor = ColumnTransformer(
transformers=[
(
"num",
Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
]),
numeric_features,
),
(
"cat",
Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore")),
]),
category_features,
),
]
)
model = Pipeline([
("preprocessor", preprocessor),
("classifier", RandomForestClassifier()),
])
C# parallel:
- Think of this like a deterministic processing pipeline where data transformation + model inference are packaged together.
- It is similar in spirit to building a well-defined application pipeline rather than scattering preprocessing logic across controllers and services.
Serving ML models via FastAPI endpoint¶
A common production approach is:
- train offline,
- serialize the model,
- load it on startup,
- expose prediction through a FastAPI endpoint.
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter()
class PredictionRequest(BaseModel):
age: int
account_balance: float
segment: str
region: str
class PredictionResponse(BaseModel):
risk_score: float
@router.post("/predict", response_model=PredictionResponse)
async def predict(payload: PredictionRequest) -> PredictionResponse:
features = [[
payload.age,
payload.account_balance,
payload.segment,
payload.region,
]]
prediction = model.predict_proba(features)[0][1]
return PredictionResponse(risk_score=float(prediction))
Production note:
- Keep the endpoint thin.
- Move model loading, feature mapping, and telemetry into dedicated services/modules.
- Treat the model artifact as a deployable dependency, not an implementation detail buried in endpoint code.
Model versioning approach¶
At minimum, version these independently:
- training code
- feature schema
- model artifact
- inference API contract
- evaluation metrics
Practical approach:
- Store model artifacts with semantic or build-based version identifiers.
- Include model version in logs and prediction responses where appropriate.
- Keep backward-compatible contracts when consumers depend on the endpoint.
- Persist metadata: training dataset snapshot, feature list, hyperparameters, and evaluation results.
For a .NET engineer, think of model versioning like versioning a deployable binary plus its contract and configuration bundle.
8. Python .NET Integration¶
When to use Python alongside .NET¶
Use Python alongside .NET when the boundary is meaningful, not merely because a team wants language variety.
Good reasons:
- Python owns ML inference or data-heavy logic.
- .NET owns domain-heavy transactional workflows.
- Python is used for fast-moving research-backed services.
- A mixed stack allows each team to use the ecosystem best suited to its problem space.
Avoid splitting across languages when:
- the service boundary adds operational overhead without business value,
- contracts are unstable,
- or the Python portion is too small to justify a separate runtime and deployment path.
gRPC communication between services¶
gRPC is a strong option when .NET and Python services need:
- strongly typed contracts,
- efficient service-to-service communication,
- generated client/server code,
- and lower overhead than JSON APIs.
Recommended approach:
- Define contracts in
.protofiles. - Generate Python and C# code from the same schema.
- Version contracts explicitly.
- Keep protobuf contracts focused on stable service boundaries.
C# parallel:
- This feels familiar if you have used ASP.NET Core gRPC with generated clients.
- The shared
.protocontract plays a role similar to a cross-language DTO contract package.
REST API integration patterns¶
REST remains appropriate when:
- interoperability matters more than performance,
- teams want easy debugging,
- external consumers are involved,
- or the integration is coarse-grained.
Recommended patterns:
- Define request/response schemas explicitly.
- Version APIs intentionally.
- Use idempotency where operations require retry safety.
- Standardize correlation IDs, auth, error envelopes, and pagination.
- Generate OpenAPI specs where possible.
FastAPI makes contract-first-ish development practical because type hints and Pydantic models automatically contribute to OpenAPI documentation.
Shared data contracts¶
Shared contracts matter more in a mixed-language estate.
You have several options:
- Protocol-first with protobuf or Avro.
- Schema-first REST with OpenAPI and generated clients.
- Event schemas using JSON Schema or a schema registry for messaging.
Good cross-language contract hygiene includes:
- explicit nullability rules,
- stable enum handling,
- date/time serialization standards,
- backward-compatible change rules,
- and example payloads for consumers.
The main lesson for .NET engineers: do not rely on language-specific assumptions such as property casing, default values, or serializer quirks. Make contracts explicit enough that Python and C# teams interpret them the same way.
9. Quick Reference Table¶
| C# concept | Python equivalent | Notes |
|---|---|---|
namespace |
package/module | Python organizes code through folders and modules rather than namespaces plus assemblies. |
.csproj project |
Python package | Often one package tree instead of many compiled projects. |
Program.cs / startup wiring |
main.py / app factory |
Composition root still matters even if the framework is lighter. |
IServiceCollection registrations |
dependency-injector providers |
Container registration is library-based rather than framework-standard. |
| Constructor injection | Depends() or explicit constructor params |
FastAPI often injects into function parameters at the edge. |
IOptions<T> |
Pydantic BaseSettings |
Strongly typed configuration binding. |
| Data annotations | Pydantic Field(...) constraints |
Structural validation close to contract definitions. |
| C# record | Pydantic model / dataclass | Pydantic adds parsing and validation. |
Task<T> |
coroutine / awaitable | Represents async work, but Python coroutine handling is easier to misuse. |
Task.WhenAll |
asyncio.gather |
Concurrently await multiple I/O operations. |
HttpClient |
httpx.AsyncClient or requests |
Prefer async client in async flows. |
| ASP.NET Core controller | FastAPI router endpoint | Thin transport layer delegating to services. |
| Middleware | FastAPI / Starlette middleware | Similar cross-cutting request pipeline concept. |
| EF Core repository / DbContext | SQLAlchemy session + repository pattern | Similar persistence layering, different APIs. |
| Interface-based abstraction | protocol / ABC / duck typing | Python often prefers structural contracts over explicit interfaces. |
| xUnit fixture | pytest fixture |
Dependency-driven test setup. |
| Moq | unittest.mock |
Dynamic mocks rather than interface-centric ones. |
| Hosted service / background worker | asyncio task, Celery worker, process worker |
Choice depends on reliability and workload type. |
| DTO + FluentValidation | Pydantic model | Contract and validation are often co-located. |
| Shared contract assembly | .proto, OpenAPI, JSON Schema |
Cross-language contracts are usually schema-based. |
Final Recommendations for .NET Engineers Moving into Python¶
- Keep the architectural discipline you learned in .NET; Python needs it just as much.
- Use type hints, linters, formatters, and tests to compensate for fewer compiler-enforced boundaries.
- Embrace Python's strengths in conciseness, ML tooling, and fast iteration without giving up maintainability.
- Prefer explicit composition and clear modules over clever metaprogramming.
- When integrating Python and .NET, invest in clear contracts and operational consistency rather than language debates.
Used well, Python becomes a strong companion to .NET rather than a replacement for it.
Related sections¶
- Code Review Guidelines for reviewing Python changes with the right quality bar.
- Engineering Principles for the underlying standards behind these conventions.
- System Design Patterns for architecture concerns that shape Python services in production.