Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to our final lesson in the module on the taxation of Spanish property sales.
In the previous lesson, we meticulously walked through the manual calculation of the total tax burden from a property sale, combining IRPF capital gains tax and plusvalía municipal to determine the after-tax IRR. That process, while essential for understanding the mechanics, can be time-consuming to repeat for every potential deal.
Today, we will bring your software engineering expertise to the forefront. This lesson is designed to help you achieve the learning outcome: Write Python code to model the total tax liability (IRPF and plusvalía) from a property sale.
We will translate the step-by-step calculations from our last session into a structured, reusable Python script. By the end of this lesson, you will have a powerful tool that can instantly calculate the tax implications of selling a property in Spain, allowing you to run scenarios and make more informed investment decisions.
Let's start coding.
1. Structuring the Model and Inputs
A well-structured model starts with clear inputs. We'll use a Python dictionary to hold all the variables for a property sale. This approach makes the code clean and easy to adapt for different properties.
Here is the data structure we'll use, populated with a hypothetical example:
# --- INPUTS ---
# All values in Euros, except where noted
investment_data = {
# Purchase Details
"purchase_price": 250000,
"purchase_costs": 20000, # (e.g., ITP, Notary, Registry fees)
"total_depreciation_taken": 18750, # Sum of all depreciation deductions during ownership
# Sale Details
"sale_price": 310000,
"selling_costs": 15500, # (e.g., Real estate agency commission)
# Plusvalía Municipal Details
"holding_period_years": 5, # Complete years of ownership
"valor_catastral_suelo": 80000, # Cadastral value of the land
"land_value_proportion": 0.4, # (valor_catastral_suelo / valor_catastral_total)
"plusvalia_tax_rate": 0.30, # Municipal tax rate (e.g., 30% in Barcelona)
}
This dictionary neatly contains all the information our model will need.
2. Modeling the IRPF Capital Gains Tax
First, let's build the functions to calculate the IRPF tax, following the logic from our previous lessons.
Step 2.1: Calculate the Adjusted Tax Basis
The first step is to determine the property's cost basis for tax purposes. This is the initial acquisition cost (valor de adquisición) minus the total depreciation you've claimed over the holding period.
def calculate_adjusted_tax_basis(purchase_price, purchase_costs, total_depreciation_taken):
"""Calculates the tax basis of the property after accounting for depreciation."""
acquisition_cost = purchase_price + purchase_costs
adjusted_basis = acquisition_cost - total_depreciation_taken
return adjusted_basis
Step 2.2: Calculate the Taxable Gain
Next, we calculate the net gain from the sale. Remember, selling costs are deductible from the sale price.

def calculate_taxable_gain(sale_price, selling_costs, adjusted_basis):
"""Calculates the net capital gain subject to IRPF tax."""
net_sale_price = sale_price - selling_costs
taxable_gain = net_sale_price - adjusted_basis
return taxable_gain
Step 2.3: Calculate the IRPF Tax Liability
With the taxable gain, we can now calculate the actual tax due. This requires implementing the progressive tax brackets for capital gains (base imponible del ahorro).
Here are the standard state-level brackets. Note that Autonomous Communities can modify these slightly, but this model provides a solid baseline.

def calculate_irpf_tax(gain):
"""Calculates the IRPF tax on a given capital gain using progressive brackets."""
if gain <= 0:
return 0
tax = 0
# Bracket 1: up to 6,000 EUR at 19%
if gain > 0:
taxable_in_bracket = min(gain, 6000)
tax += taxable_in_bracket * 0.19
gain -= taxable_in_bracket
# Bracket 2: 6,000 to 50,000 EUR at 21%
if gain > 0:
taxable_in_bracket = min(gain, 50000 - 6000)
tax += taxable_in_bracket * 0.21
gain -= taxable_in_bracket
# Bracket 3: 50,000 to 200,000 EUR at 23%
if gain > 0:
taxable_in_bracket = min(gain, 200000 - 50000)
tax += taxable_in_bracket * 0.23
gain -= taxable_in_bracket
# Bracket 4: over 200,000 EUR at 27% (for 2024, there are more brackets, we simplify here)
# The image shows up to 23%, let's add the known higher brackets for more accuracy.
# Bracket 4: 200,000 to 300,000 EUR at 27%
if gain > 0:
taxable_in_bracket = min(gain, 300000 - 200000)
tax += taxable_in_bracket * 0.27
gain -= taxable_in_bracket
# Bracket 5: over 300,000 EUR at 28%
if gain > 0:
tax += gain * 0.28
return tax
3. Modeling the Plusvalía Municipal Tax
Now for the plusvalía municipal. As we learned, the law allows taxpayers to choose between two calculation methods: the "objective" method based on cadastral value and coefficients, and the "real gain" method. Our model must calculate both and pick the lower one.
To understand the logic we're about to code, it's helpful to review the detailed rules.
La plusvalía municipal: cómo se calcula
Please read the following sections from the article 'La plusvalía municipal: cómo se calcula' by Notarías Barcelona. This will provide the foundation for the two calculation methods we will implement in our Python code.
Focus on the two main sections detailing the calculation methods: '1.1. Método de estimación objetiva' and '1.2. Método de estimación directa o real'. Pay close attention to the inputs for each method (cadastral value, coefficients, purchase/sale prices) as we will directly translate these into code.
Step 3.1: The Objective Method (Método Objetivo)
This method multiplies the valor catastral del suelo by a coefficient determined by the holding period.
def calculate_plusvalia_objective(valor_catastral_suelo, holding_period_years, tax_rate):
"""Calculates plusvalía tax using the objective method (coefficients)."""
# Coefficients based on Real Decreto-ley 26/2021 (Ayuntamientos can set their own, up to these max values)
# We will use the maximums as a conservative default.
coeffs = {
0: 0.15, 1: 0.15, 2: 0.14, 3: 0.14, 4: 0.16, 5: 0.18, 6: 0.19, 7: 0.20,
8: 0.19, 9: 0.15, 10: 0.12, 11: 0.10, 12: 0.09, 13: 0.09, 14: 0.09,
15: 0.09, 16: 0.10, 17: 0.13, 18: 0.17, 19: 0.23, 20: 0.40
}
# Get coefficient for holding period, capped at 20 years
period = min(holding_period_years, 20)
# For periods less than 1 year, the coefficient is for period 0
if holding_period_years < 1:
period = 0
coefficient = coeffs.get(period, 0.40) # Default to 20+ year coeff if not found
taxable_base = valor_catastral_suelo * coefficient
tax = taxable_base * tax_rate
return tax
Step 3.2: The Real Gain Method (Método de Estimación Directa)
This method calculates the actual gain and applies the land's proportion of the total cadastral value.
def calculate_plusvalia_real_gain(purchase_price, sale_price, land_value_proportion, tax_rate):
"""Calculates plusvalía tax using the real gain method."""
total_gain = sale_price - purchase_price
if total_gain <= 0:
return 0 # No tax if there was no gain
taxable_base = total_gain * land_value_proportion
tax = taxable_base * tax_rate
return tax
Step 3.3: The Final Plusvalía Municipal Calculation
Finally, a wrapper function that calculates both and returns the minimum, just as a taxpayer would choose.
def calculate_plusvalia_municipal(data):
"""Calculates the final plusvalía municipal tax by choosing the cheaper method."""
# Method 1: Objective
tax_objective = calculate_plusvalia_objective(
data["valor_catastral_suelo"],
data["holding_period_years"],
data["plusvalia_tax_rate"]
)
# Method 2: Real Gain
tax_real_gain = calculate_plusvalia_real_gain(
data["purchase_price"],
data["sale_price"],
data["land_value_proportion"],
data["plusvalia_tax_rate"]
)
# Taxpayer chooses the minimum of the two. If real gain is negative, the tax is 0.
final_tax = min(tax_objective, tax_real_gain)
# A crucial rule: if the real gain is zero or less, there is NO tax, regardless of the objective method's result.
if data["sale_price"] <= data["purchase_price"]:
return 0
return final_tax
4. Bringing It All Together
Now we can create a main script that uses our investment_data dictionary and these functions to compute the total tax liability.
# --- MAIN CALCULATION SCRIPT ---
def calculate_total_sale_tax_liability(data):
"""
Calculates the total tax liability (IRPF + Plusvalía) from a property sale.
"""
# --- IRPF Calculation ---
basis = calculate_adjusted_tax_basis(
data["purchase_price"],
data["purchase_costs"],
data["total_depreciation_taken"]
)
gain = calculate_taxable_gain(
data["sale_price"],
data["selling_costs"],
basis
)
irpf_tax = calculate_irpf_tax(gain)
# --- Plusvalía Municipal Calculation ---
plusvalia_tax = calculate_plusvalia_municipal(data)
# --- Total Liability ---
total_tax = irpf_tax + plusvalia_tax
# --- Print Results ---
print("--- Property Sale Tax Calculation ---")
print(f"Taxable Capital Gain (IRPF): {gain:,.2f} €")
print(f"IRPF Tax Liability: {irpf_tax:,.2f} €")
print("-" * 35)
print(f"Plusvalía Municipal Tax: {plusvalia_tax:,.2f} €")
print("-" * 35)
print(f"TOTAL TAX LIABILITY: {total_tax:,.2f} €")
return {
"irpf_tax": irpf_tax,
"plusvalia_tax": plusvalia_tax,
"total_tax": total_tax
}
# Run the calculation with our example data
calculate_total_sale_tax_liability(investment_data)
Running this script with our example data will produce the following output:
--- Property Sale Tax Calculation ---
Taxable Capital Gain (IRPF): 26,250.00 €
IRPF Tax Liability: 5,392.50 €
-----------------------------------
Plusvalía Municipal Tax: 4,320.00 €
-----------------------------------
TOTAL TAX LIABILITY: 9,712.50 €
Test your understanding!
Using the investment_data from our example, what would happen to the plusvalía municipal tax if the sale_price was €240,000 instead of €310,000 (i.e., the property was sold at a loss)? You don't need to run the code, just reason through the logic of the calculate_plusvalia_municipal function.
Show answer
The plusvalía municipal tax would be €0. The calculate_plusvalia_municipal function contains a specific check: if the sale price is less than or equal to the purchase price, it immediately returns 0. This reflects the constitutional court ruling that you cannot be taxed if you haven't realized an actual economic gain, regardless of what the "objective" method might calculate based on the land's cadastral value increase.
Conclusion
Congratulations! You have successfully built a Python model that automates the complex tax calculations involved in selling a property in Spain. This script is not just an academic exercise; it's a practical tool you can use to evaluate exit scenarios for any potential investment.
Key Takeaways:
- Modular Code is Reusable: By breaking the problem down into small, single-purpose functions, we've created a model that is easy to read, debug, and adapt.
- Modeling Mirrors Regulation: Our code directly implements the rules for IRPF capital gains and the dual-method system for
plusvalía municipal, providing a realistic tax estimate. - Automation Enables Analysis: With this script, you can now instantly analyze how changes in sale price, holding period, or selling costs affect your total tax bill and, consequently, your net proceeds and overall return.
Preview of the Next Module:
This lesson concludes our deep dive into the taxation of real estate operations and dispositions. In our next module, "Advanced Spanish Tax and Ownership Structures," we will take this analysis to an even higher level. We'll explore topics like the Spanish Wealth Tax (Impuesto sobre el Patrimonio) and, crucially, model the financial differences between owning property as an individual versus through a Spanish Sociedad Limitada (LLC). The code we've built today will serve as a foundational block for those more advanced comparisons.