1. Introduction
Now is the time to learn Python if you are an ASP.NET Core developer.
Python is the number one programming language to know when working on AI projects. It dominates the field with 70% adoption in AI and machine learning projects, according to index.dev.
I have been an ASP.NET developer for over 10 years working with C# and SQL Server. But, in the last 2 years I've been developing with Python and I'm blown away with it's versatility, array of libraries, and the community, which has an answer for just about any exception thrown.
If you’re a C# developer, you already understand core software principles. Learning Python is more about learning how things are done differently rather than what is being done. With the growing demand for AI integrations, automation, and flexible APIs, Python is an invaluable skill that will amplify your capabilities and position you as a cross-platform developer.
What you'll hate about Python
Lack of Static Type Enforcements - in C# you have strong compile-time checks that prevent errors from occurring early on. Python is strongly typed, just not statically typed. Therefore, it's hard to identify these errors at compile time and you'll catch them sneak up at runtime.
x = 5
print(type(x)) # Output: <class 'int'>
x = "Hello"
print(type(x)) # Output: <class 'str'>
x = [1, 2, 3]
print(type(x)) # Output: <class 'list'>
There are libraries such as pydantic.dev that enforces type validation and define models with strict type validation.
What you'll love about Python
Massive Ecosystem of Libraries - from data science (NumPy, pandas, scikit-learn) to web development (Flask, Django, FastAPI) to automation (fabric, Invoke), Python’s ecosystem is deep and varied. In addition, the strong community and support that continually contribute to new and existing libraries.
Rich Data Science and AI/ML Tools - python is the go-to language for machine learning and data analysis, providing access to powerful libraries like TensorFlow, PyTorch, and spaCy.
To help you understand Python from the perspective of an aspnet core developer, this post will cover setting up each layer of an API project with C# first and then it's Python equivalent.
To help you understand Python from the perspective of a C# developer each section below will compare a typical ASP.NET core API concept to its Python equivalent, providing code snippets and best practices.
2. Basic Project Structure and Entry Points
The entry point for a ASP.NET Core project is Program.cs, where app configurations are made such as middleware (routing, authentication, and more) and dependency injection. For Python it is main.py.
- C#:
Program.cs
(and.csproj
for project configuration) - Python:
main.py
(andrequirements.txt
orpyproject.toml
for dependencies)
Starting with .NET 6, a “minimal” hosting model became available, drastically reducing boilerplate. The Program.cs
file essentially is the entry point. A simple application might look like this:
C#// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Configure services if needed
// builder.Services.AddControllers();
// builder.Services.AddDbContext<MyDbContext>(...);
// etc.
var app = builder.Build();
// Map endpoints (Minimal API style)
app.MapGet("/", () => "Hello World!");
// Finally, run the app
app.Run();
Key Points
- Single File: In minimal APIs, your entire application entry point (and even some routing) can live in
Program.cs
. - Implicit
Main
Method: You don’t explicitly see astatic void Main(string[] args)
method; the compiler generates it behind the scenes. - Project File: You’ll still have a
.csproj
file that references your dependencies, SDK version, and metadata about your project. - Configuration and Services: Typically,
builder.Services
is where you’d register services (like EF Core, logging, or custom DI). - Flexibility: You can still break out code into multiple classes (e.g., separate files for
Routes
orServices
), but minimal APIs allow you to keep things very concise.
Unlike C#’s official minimal API pattern, Python has no single “official” structure. However, most small-to-medium projects tend to follow this layout:
Python
my_python_app/
├─ requirements.txt (or pyproject.toml)
├─ main.py (entry point)
├─ app/
│ ├─ __init__.py
│ └─ routes.py (if using Flask/FastAPI)
└─ ...# main.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "Hello World!"
if __name__ == "__main__":
# Start the Flask development server
app.run(debug=True)
Key Points
if __name__ == "__main__":
This line ensures the code block only runs whenmain.py
is called directly (not when it’s imported as a module). This is the closest analog to a “Main” method in Python.- Dependencies
Typically listed in arequirements.txt
(forpip
) or in apyproject.toml
/Pipfile
(for Poetry or Pipenv). For example, arequirements.txt
might have:
Flask==2.3.2
Then you’d install them using
requests==2.31.0
SQLAlchemy==2.0.18pip install -r requirements.txt
.
- Project Structure
- Often you create an
app/
orsrc/
folder to keep your routes, models, utilities, etc. main.py
(or sometimesapp.py
) is just an entry point that imports and initializes the core application.
- Often you create an
Here's another example with FastAPI# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello World!"}
if __name__ == "__main__":
import uvicorn
# uvicorn is a common ASGI server used for FastAPI apps
uvicorn.run(app, host="0.0.0.0", port=8000)
Comparing the Two Approaches
Aspect | ASP.NET Core (.NET 6+) | Python (Flask / FastAPI) |
---|---|---|
Entry Point | Program.cs (minimal API) | main.py with if __name__ == "__main__": |
Project File | .csproj (specifies SDK, deps) | requirements.txt or pyproject.toml |
Dependency Management | NuGet packages | pip (PyPI), Poetry, Pipenv |
Configuration Style | Typically done in builder. Configuration or Startup class | Typically environment variables, .env, or config files loaded in code |
Routing | Attributes or minimal route mapping | Decorators in Flask/FastAPI |
Hosting | Built-in Kestrel web server; can run behind IIS/Azure | Gunicorn/Uvicorn for production; also behind Nginx or Docker |
Observations
- .NET 6 Minimal APIs and Flask/FastAPI share the idea of a single file that “runs” your application, plus separate modules for routes, models, etc.
- Dependency Installation is done via
dotnet add package <PackageName>
in C# vs.pip install <package>
in Python. - Deployment can often revolve around Docker containers in both ecosystems, but the actual commands differ (
dotnet publish
,pip install -r requirements.txt
). - Configuration in .NET is more structured (
appsettings.json
) while Python tends to rely heavily on environment variables and.env
files (though third-party packages can mimic the structured approach).
3. Web Frameworks
Whether you’re building an ASP.NET Core Web API or a Python web service, the core principles remain consistent: you define routes, process HTTP requests, manipulate data, and return structured responses (usually JSON). The main difference lies in language syntax, configuration style, and how each framework organizes routes and middleware. In C#, ASP.NET Core is a robust framework with a built-in HTTP pipeline, powerful dependency injection, and convention-based or minimal APIs. Meanwhile, Python features multiple “micro” or “full-stack” frameworks (Flask, FastAPI, Django) that similarly give you route decorators, request handling, and the ability to scale with additional libraries.
Let's break these into three similarities:
1. Minimal API (C#) vs Flask (Python)
2. Controllers (C#) vs. Blueprints or Routers (Python)
3. Middleware & Pipeline
1. Minimal API (C#) vs. Flask (Python)
ASP.NET Core (Minimal API Example)
In .NET 6 and beyond, you can create a minimalist setup in Program.cs
:
C#var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Define a simple "Hello, World!" route
app.MapGet("/", () => "Hello, World!");
// Start the application
app.Run();
Explanation
- WebApplication: The minimal API approach removes the need for separate
Startup.cs
or an explicitMain
method. - Routing:
app.MapGet
maps an HTTP GET request to the root URL ("/"
). - Return Value: The string
"Hello, World!"
is automatically serialized into an HTTP response.
A similarly concise approach in Python using Flask might look like:
Flask (Python)
from flask import Flask
app = Flask(__name__)
@app.route("/", methods=["GET"])
def hello_world():
return "Hello, World!"
if __name__ == "__main__":
app.run(debug=True)
Explanation
- Flask App:
Flask(__name__)
initializes a lightweight web application. - Route Decorator: The
@app.route("/", methods=["GET"])
decorator is how you define a URL endpoint. - Return Value: Returning
"Hello, World!"
sends that string as the HTTP response. - Entry Point: The
if __name__ == "__main__":
check ensures this file runs directly. It callsapp.run()
to launch a development server.
2. Controllers (C#) vs. Blueprints or Routers (Python)
C#using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
// Normally fetch data from a database or service
var products = new[] { new { Id = 1, Name = "Laptop" }, new { Id = 2, Name = "Mouse" } };
return Ok(products);
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
// Return a single product
var product = new { Id = id, Name = "Placeholder" };
return Ok(product);
}
}
Explanation
- Controller Attribute:
[ApiController]
plus the[Route("api/[controller]")]
attribute define how requests to"/api/products"
are routed. - Action Methods:
GetAll()
andGet(int id)
handle different URIs ("/api/products"
vs."/api/products/{id}"
). - Return Types:
IActionResult
or typed results (likeActionResult<IEnumerable<Product>>
) let you return HTTP responses with codes and data.
Flask (Blueprints) or FastAPI (Router)
To mimic a more modular, “controller-like” structure in Python, you can split routes into separate modules. Below is an example with FastAPI using a router:
Python# product_routes.py
from fastapi import APIRouter
router = APIRouter(prefix="/api/products", tags=["products"])
@router.get("/")
def get_all_products():
return [
{"id": 1, "name": "Laptop"},
{"id": 2, "name": "Mouse"}
]
@router.get("/{id}")
def get_product(id: int):
return {"id": id, "name": "Placeholder"}
# main.py
from fastapi import FastAPI
from product_routes import router as product_router
app = FastAPI()
app.include_router(product_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Explanation
- APIRouter:
APIRouter
organizes endpoints similarly to a “controller.” - Prefix:
prefix="/api/products"
is comparable to[Route("api/products")]
. - Method-Specific Functions:
@router.get("/")
parallels[HttpGet]
. - Include Router: In your main app,
app.include_router(product_router)
is how you “register” these routes.
3. Middleware & Pipeline
Both ASP.NET Core and Python’s microframeworks allow you to define middleware to process requests before and after they hit your route handlers:
- C#:
app.UseMiddleware<CustomMiddleware>()
in the minimal API orStartup.Configure()
method. - Python: Flask supports request hooks (
before_request
,after_request
), or you can write custom middlewares in FastAPI by using dependency injection or event handlers.
C#
app.Use(async (context, next) =>
{
// Pre-processing
await next.Invoke();
// Post-processing
});
Python (FastAPI)
from fastapi import Request, FastAPI
app = FastAPI()
@app.middleware("http")
async def custom_middleware(request: Request, call_next):
# Pre-processing
response = await call_next(request)
# Post-processing
return response
Summary of Similarities
- Routing
- C# uses attributes or minimal route mapping, while Python uses decorators. Both define HTTP methods and URLs in a similarly direct manner.
- Controllers vs. Modules/Blueprints
- C# organizes routes into Controllers. Python frameworks often encourage smaller “blueprints” or “routers,” achieving a similar modular structure.
- Return Values
- Both frameworks let you easily return JSON, automatically handling serialization and status codes (with slightly different syntax).
- Middleware
- Both ecosystems feature a pipeline where you can intercept requests.
- Scalability
- ASP.NET Core and Python frameworks both scale from small prototypes to large distributed systems. The difference is largely in how you approach the ecosystem (NuGet vs. PyPI packages, built-in vs. third-party solutions).
- ASP.NET Core and Python frameworks both scale from small prototypes to large distributed systems. The difference is largely in how you approach the ecosystem (NuGet vs. PyPI packages, built-in vs. third-party solutions).
4. Models, DTOs, and Validation
Plain Old C# Objects (POCOs) are used to transfer data between different layers of your architecture and data annotations for setting property boundaries. To achieve the same in Python, Pydantic is used for strong typing and validation in a simplified approach.
As an example, the WeatherForecast has three properties: Date, Temperature and Comments. Each property can be recreated from C# to python using Pydantic:
C#
public class WeatherForecast
{
[Required]
public DateTime Date { get; set; }
[Range(-100, 150)]
public int TemperatureC { get; set; }
public string? Comments { get; set; }
}
Python
from pydantic import BaseModel, Field
from datetime import datetime
class WeatherForecast(BaseModel):
date: datetime
temperature_c: int = Field(..., ge=-100, le=150) comments: Optional[str] = None
In this example the WeatherForecast has three properties and each defined using Pydantic's notation. Both date and comments are self explanatory but temperature_c is a bit different. The triple dots (...) is considered a Python Ellipsis object, which Pyndatic uses as a special object value to mark a field as required.
5. Data Access and ORMs
Entity Framework, both classic and Core, is the most widely used framework for data access and Object-Relational Mapping in C#. It combines both data access and relational mapping within a few libraries. In addition, depending on the database, those libraries maybe specific to either MySql, Postgres, SqlServer or others.
The different parts of connecting to a database can be broken down into three parts:
- Data Connector (MySql, Sql Server, etc.)
- Data Access (low-level control i/o to the database)
- ORM (classes and objects of the tables, rows, and columns) (EF)
If you're using an ORM, the Data Access is abstracted away. The Data Connector is always needed whether you are using an ORM or low-level data access.
Data Connector
Python has different data connectors based on the different databases you're targeting, these connectors follow a standard interface called DB-API (PEP 249). Depending on what database you are connecting to you can use either of the following connectors:
- SQLite:
sqlite3
(built into Python) - PostgreSQL:
psycopg2
,asyncpg
- MySQL:
mysql-connector-python
,PyMySQL
- SQL Server:
pyodbc
(with the appropriate driver)
Installing pyodbc on an Apple silicon chip can be troublesome, if you run into problems reach out to me or drop a line in the comments below.
Data Access
In C# if you plan on low-level data access your typical default would normally be System.Data.SqlClient. This would be used to create the connection to the database and execute a sql statement.
Below is an example of this:
using System;
using System.Data;
using System.Data.SqlClient;
namespace DataAccessExample
{
class Program
{
static void Main(string[] args)
{
// 1. Define your connection string
// Adjust the server, database, and credentials as appropriate.
string connectionString = "Server=YOUR_SERVER_NAME;Database=YOUR_DATABASE_NAME;User Id=YOUR_USERNAME;Password=YOUR_PASSWORD;";
// 2. Create a connection object and open it
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
string selectCommandText = "SELECT Id, Name, Email FROM Users";
using (SqlCommand selectCmd = new SqlCommand(selectCommandText, conn))
{
using (SqlDataReader reader = selectCmd.ExecuteReader())
{
while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
string email = reader.GetString(2);
Console.WriteLine($"ID: {id}, Name: {name}, Email: {email}");
}
}
}
}
}
}
}
Now in Python, using pyodbc to connect to a Sql Server database. import pyodbc
# 1. Connect to a SQL Server database.
conn_string = (
"Driver={ODBC Driver 17 for SQL Server};"
"Server=YOUR_SERVER_NAME_HERE;"
"Database=YOUR_DATABASE_NAME_HERE;"
"UID=YOUR_USERNAME_HERE;"
"PWD=YOUR_PASSWORD_HERE;"
)
conn = pyodbc.connect(conn_string)
cursor = conn.cursor()
# 2. Query data
select_query = 'SELECT id, name, email FROM users'
cursor.execute(select_query)
rows = cursor.fetchall()
# 3. Display the results
for row in rows:
print(f"ID: {row[0]}, Name: {row[1]}, Email: {row[2]}")
# 4. Close cursor and connection
cursor.close()
conn.close()
Let's break this down:
import pyodbc
- this is the Python driver/connector for SQL Server that follows the PEP 249 DB-API standard.
conn = pyodbc.connect(conn_string)
- create the connection
cursor = conn.cursor()
- create a cursor object that will allow you to execute SQL statements
cursor.execute
- set your sql statement
rows = cursor.fetchall()
- gets all results in a list
ORM
Next, we'll use SQLAlchemy, an Object-Relational Mapper and SQL Toolkit, to create a connection and query the users table.
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
# 1. Define the Base class for our ORM models
Base = declarative_base()
# 2. Create a User model that matches the 'users' table schema
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
email = Column(String(200), nullable=False)
# 3. Create the SQLAlchemy engine for SQL Server
# Adjust the connection string to match your environment
connection_string = (
"mssql+pyodbc://USERNAME:PASSWORD@SERVER_HOST:1433/DATABASE_NAME?"
"driver=ODBC+Driver+17+for+SQL+Server"
)
engine = create_engine(connection_string)
# 4. Create a session factory and instantiate a session
Session = sessionmaker(bind=engine)
session = Session()
try:
# 5. Query the 'users' table
users = session.query(User).all()
# 6. Display the results
for user in users:
print(f"ID: {user.id}, Name: {user.name}, Email: {user.email}")
finally:
# 7. Close the session (and its connection)
session.close()
Similar to EF, SQLAlchemy is used to abstract the low-level code and make data access more manageable.
If you're interested in learning about data migrations, I suggest taking a look at Alembic. Alembic is a database migrations tool written by the author of SQLAlchemy.
6. Routing
Registering routes in ASP.NET happen in the Prorgam.cs class, and then the routes can be defined by using either Conventional or Attribute Routing patterns. For this example let's setup a route to retrieve a car from endpoint: "../api/cars/{id}".
Using Attribute Routing, we will first register the routing middleware in Program.cs and then setup a route for the CarsController.
Registering routes in Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add controller services with routingbuilder.Services.AddControllers();
var app = builder.Build();
// Configure the routing middleware
app.UseRouting();
// Map controller endpoints
app.MapControllers();
app.Run();
Cars controller CarsController.cs[Route("api/[controller]")]
public class CarsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id) { ... }
}
(or)[Route("api/cars")]
public class CarsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id) { ... }
}
Let's implement the same route in Python, using FastAPI. You'll see that approach is similar. If we group our endpoints into routes, which would be similar to Controllers in C#, then we must include the routers when setting up the app. At the router level, the route "cars" is defined in the constructor instead of placing an attribute on the class.
Python
from fastapi import APIRouter, FastAPI, Depends, Header, HTTP Exception
from typing
import Optional
# Create a FastAPI instance
app = FastAPI(
title="My First FastAPI App",
description="A simple API with a hello world endpoint",
version="0.1.0"
)
# Can be defined in another file Router/cars.py
class CarsRouter:
def __init__(self):
self.router = APIRouter(
prefix="/api/cars",
tags=["cars"],
responses={404: {"description": "Not found"}},
)
# Define the route with the function
self.router.get("/{id}")(self.get_car)
async def get_car(self, id: int):
return id
cars_router = CarsRouter()
app.include_router(cars_router.router)
We now have routes setup in our app, we could have further organized this better by creating a router folder and adding the CarsRouter within the router folder.
7. Dependency Injection
In asp.net Dependency Injection (DI) is setup in Program.cs and it is a built-in dependency injection framework part of ASP.NET Core. Furthermore, we have control over whether it is Transient, Scoped or a Singleton. Let's extend our sample code from the Router section above and inject a Database Service with an Interface. We'll start off with an example in C#.
C#// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddControllers();
builder.Services.AddSingleton<IDatabaseService, DatabaseService>(); // Register our database service
var app = builder.Build();
// Configure the middleware pipeline
app.UseRouting();
app.UseAuthorization();
// Configure endpoints
app.MapControllers();
app.Run();
// IDatabaseService.cs
public interface IDatabaseService
{
List<Car> GetAllCars();
Car GetCarById(int id);
}
// DatabaseService.cs
public class DatabaseService : IDatabaseService
{
private readonly List<Car> _cars = new List<Car>
{
new Car { Id = 1, Name = "Honda Accord", Year = 2024 },
new Car { Id = 2, Name = "Toyota Corolla", Price = 2022 },
new Car { Id = 3, Name = "Toyota Camry", Price = 2021 }
};
public List<Car> GetAllCars()
{
return _cars;
}
public Car GetCarById(int id)
{
return _cars.FirstOrDefault(p => p.Id == id);
}
}
// Car.cs
public class Car
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// CarsController.cs
[ApiController]
[Route("api/[controller]")]
public class CarsController : ControllerBase
{
private readonly IDatabaseService _databaseService;
// Constructor injection of our database service
public CarsController(IDatabaseService databaseService)
{
_databaseService = databaseService;
}
[HttpGet]
public ActionResult<IEnumerable<Product>> GetAll()
{
return Ok(_databaseService.GetAllCars());
}
[HttpGet("{id}")]
public ActionResult<Product> Get(int id)
{
var product = _databaseService.GetCarById(id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
To implement the same Database Service in Python we will continue to use FastAPI, which happens to have built-in DI.
We'll extend our router from above and inject a Database service that returns a list of cars. Notice that we don't need to register the DI like we do in an asp.net project. However, defining the service as either Transient, Scoped, or Singleton is different and is determined based on how it is implemented. Let's take a look at how a Singleton Database Service is created.
Python
# db/database.py
class DatabaseService:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(DatabaseService, cls).__new__(cls)
# Initialize the instance here
cls._instance.connection_string = kwargs.get('connection_string', 'default')
cls._instance._cars = {
1: {"id": 1, "make": "Toyota", "model": "Camry"},
2: {"id": 2, "make": "Honda", "model": "Civic"}
}
return cls._instance
def get_all_cars(self):
return list(self._cars.values())
def get_car(self, id):
return self._cars.get(car_id)
The __new__ method in DatabaseService controls instance creation and subsequent calls to the constructor will return the existing instance.
Next is to inject the service into the router.
from fastapi import APIRouter, FastAPI, Depends, Header, HTTP Exception
from typing
import Optional
from ..db.database import DatabaseService
# Create a FastAPI instance
app = FastAPI(
title="My First FastAPI App",
description="A simple API with a hello world endpoint",
version="0.1.0"
)
# Can be defined in another file Router/cars.py
class CarsRouter:
def __init__(self, db_service: DatabaseService):
self.db_service = db_service
self.router = APIRouter(
prefix="/api/cars",
tags=["cars"],
responses={404: {"description": "Not found"}},
)
# Define the route with the function
self.router.get("/{id}")(self.get_car)
async def get_car(self, id: int):
return self.db_service.get_car(id)
# Get the singleton instance of DatabaseService
db_service = DatabaseService(connection_string="production_db")
cars_router = CarsRouter(db_service)
app.include_router(cars_router.router)
8. Configuration and Environment Management
Every application typically needs different settings for development, testing, staging, and production environments (e.g., database connections, API keys, debugging flags). Managing these configurations cleanly helps ensure that code can be easily deployed in various environments without constant manual tweaks.
ASP.NET Core projects commonly use:
- appsettings.json: Holds default configuration.
- appsettings.Development.json (or Staging, Production, etc.): Contains environment-specific overrides.
- Environment Variables: For sensitive data (like passwords or API tokens) and containerized deployment.
C#var builder = WebApplication.CreateBuilder(args);
// Reads appsettings.json, then appsettings.<EnvironmentName>.json (if present),
// and finally environment variables, in that order.
builder.Configuration
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json",
optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
// Access configuration within the app.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var app = builder.Build();
// ...
app.Run();
- Order of Configuration: ASP.NET Core merges multiple sources in a certain order. The last source added can override previous ones, making environment variables a convenient final override for secrets or server-specific values.
- Environment Names: Typically set using the
ASPNETCORE_ENVIRONMENT
environment variable. Common names areDevelopment
,Staging
, andProduction
.
Here's an example configuration of appsettings.json{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyDb;User=sa;Password=Pass@123;"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
Environment Variables
At deployment or container runtime, you can set:ASPNETCORE_ENVIRONMENT=Production
ConnectionStrings__DefaultConnection=Server=prod-db;Database=MyDb;User=sa;Password=RealSecret;
ASP.NET Core automatically maps __
(double underscore) to nested JSON properties
Python Approach
Python doesn’t have a single standard configuration mechanism built into the language (like appsettings.json
in ASP.NET Core). Instead, developers commonly rely on:
- Environment variables
- A
.env
file (loaded by packages likepython-dotenv
) - Configuration libraries (e.g.,
dynaconf
,configparser
, or Pydantic’s “Settings”)
Furthermore, we also use external libraries such as dotenv to load environment variables with the method load_dotenv. Let's step through how we would load a .env file in code.
dotenv Example
- Create a
.env
file in your project (which you should add to.gitignore
so it isn’t tracked in version control):
ENVIRONMENT=Development
DB_URL=postgresql://user:pass@localhost:5432/mydb
SECRET_KEY=SuperSecret123
- Load environment variables in your Python code (e.g.,
main.py
):
import os
from dotenv import load_dotenv
# Load from .env
load_dotenv()
# Now environment variables are available via os.getenv
ENVIRONMENT = os.getenv("ENVIRONMENT", "Development")
DB_URL = os.getenv("DB_URL", "sqlite:///:memory:")
SECRET_KEY = os.getenv("SECRET_KEY", "default_secret")
print("Current ENV:", ENVIRONMENT)
print("DB URL:", DB_URL)
You can also create different .env files for development, staging, and production:
.env.development
.env.staging
.env.production
Here is an example of specifying .env.production
in Docker Compose
version: '3.8'
services:
web:
build: .
env_file:
- .env.production
ports:
- "80:80"
Summary
Transitioning from ASP.NET Core to Python doesn’t mean abandoning the .NET ecosystem—it means expanding your toolkit to meet the evolving demands of modern software development, especially in AI and data-driven applications. As we’ve seen, concepts like routing, middleware, dependency injection, and data access have direct parallels between the two languages. The difference lies in syntax, structure, and the community-driven philosophies behind them.
Whether you're automating tasks, exploring machine learning, or building microservices, Python is a worthy companion to your .NET skillset.
Ready to build your first Python API? Let me know in the comments what topic you'd like covered next—authentication, background tasks, or deploying to the cloud.