Executive Summary

This analysis calculates French capital gains tax liability for a French tax resident selling German property. The analysis examines how the total tax burden (income tax + social levies) decreases over the holding period due to time-based allowances.

Tax Jurisdiction

German Tax Status: As the property has been held for more than 10 years, there is NO German capital gains tax liability under German tax law (§23 EStG - Einkommensteuergesetz). Germany exempts real estate capital gains for properties held longer than 10 years.

French Tax Liability: However, as a French tax resident, the seller remains subject to French capital gains tax on worldwide assets, including this German property. This analysis focuses exclusively on the French tax liability.


Scenario Parameters

# Define the scenario
selling_price <- 550000  # Sale price in euros
cost_base <- 250000      # Original purchase price (NOT inflation-adjusted)
capital_gain <- selling_price - cost_base

cat("Selling Price: €", format(selling_price, big.mark = ","), "\n")
## Selling Price: € 550,000
cat("Cost Base (Original Purchase): €", format(cost_base, big.mark = ","), "\n")
## Cost Base (Original Purchase): € 250,000
cat("Capital Gain (Nominal): €", format(capital_gain, big.mark = ","), "\n")
## Capital Gain (Nominal): € 300,000

Critical Note: The €300,000 gain is the nominal difference between sale price and original cost base. Per French tax law, there is no adjustment for inflation in this calculation.


Implementation

library(ggplot2)
library(dplyr)
library(knitr)
library(scales)
# Function to calculate French taxes on property capital gains
# Based on official rates from impots.gouv.fr (July 2022)
calculate_taxes <- function(selling_price, cost_base, years_range) {
  
  # Calculate the capital gain (NOT inflation-adjusted per French tax law)
  gain <- selling_price - cost_base
  
  results <- data.frame(
    Years_Held = integer(),
    IR_Allowance_Pct = numeric(),
    PS_Allowance_Pct = numeric(),
    Income_Tax = numeric(),
    Social_Levies = numeric(),
    Total_Tax = numeric(),
    Effective_Rate = numeric()
  )
  
  for (n in years_range) {
    
    # Income Tax (IR) Allowance - Official rates from impots.gouv.fr
    # 6% per year from year 6-21, then 4% in year 22 for full exemption
    if (n <= 5) {
      ir_allowance <- 0
    } else if (n <= 21) {
      ir_allowance <- (n - 5) * 0.06
    } else {
      ir_allowance <- 1.0  # Full exemption after 22 years
    }
    
    # Social Levies (PS) Allowance - Official rates from impots.gouv.fr
    # 1.65% per year from year 6-21
    # 1.60% in year 22 (bringing total to 28%)
    # 9% per year from year 23-30 for full exemption
    if (n <= 5) {
      ps_allowance <- 0
    } else if (n <= 21) {
      ps_allowance <- (n - 5) * 0.0165
    } else if (n == 22) {
      # Year 22: prior allowance (26.4%) + 1.6% = 28%
      ps_allowance <- 0.28
    } else if (n <= 30) {
      # Years 23-30: 28% base + 9% per additional year
      ps_allowance <- 0.28 + (n - 22) * 0.09
    } else {
      ps_allowance <- 1.0  # Full exemption after 30 years
    }
    
    # Calculate taxes on NOMINAL gain (no inflation adjustment)
    # IR rate: 19% (official rate from impots.gouv.fr)
    # PS rate: 17.2% (official rate from impots.gouv.fr)
    income_tax <- max(0, gain * (1 - ir_allowance) * 0.19)
    social_levies <- max(0, gain * (1 - ps_allowance) * 0.172)
    total_tax <- income_tax + social_levies
    
    results <- rbind(results, data.frame(
      Years_Held = n,
      IR_Allowance_Pct = ir_allowance * 100,
      PS_Allowance_Pct = ps_allowance * 100,
      Income_Tax = income_tax,
      Social_Levies = social_levies,
      Total_Tax = total_tax,
      Effective_Rate = (total_tax / gain) * 100
    ))
  }
  
  return(results)
}

Results

# Calculate taxes for holding periods from 12 to 30 years
df <- calculate_taxes(
  selling_price = selling_price,
  cost_base = cost_base,
  years_range = 12:30
)

# Save to CSV
write.csv(df, 'german_property_total_tax.csv', row.names = FALSE)

Summary Table

# Display detailed results
kable(df, 
      format.args = list(big.mark = ",", digits = 2, scientific = FALSE),
      caption = "French Tax Liability by Holding Period (Based on Official impots.gouv.fr Rates)",
      col.names = c("Years Held", "IR Allowance %", "PS Allowance %", 
                    "Income Tax (€)", "Social Levies (€)", 
                    "Total Tax (€)", "Effective Rate %"))
French Tax Liability by Holding Period (Based on Official impots.gouv.fr Rates)
Years Held IR Allowance % PS Allowance % Income Tax (€) Social Levies (€) Total Tax (€) Effective Rate %
12 42 12 33,060 45,640 78,700 26.2
13 48 13 29,640 44,789 74,429 24.8
14 54 15 26,220 43,937 70,157 23.4
15 60 16 22,800 43,086 65,886 22.0
16 66 18 19,380 42,235 61,615 20.5
17 72 20 15,960 41,383 57,343 19.1
18 78 21 12,540 40,532 53,072 17.7
19 84 23 9,120 39,680 48,800 16.3
20 90 25 5,700 38,829 44,529 14.8
21 96 26 2,280 37,978 40,258 13.4
22 100 28 0 37,152 37,152 12.4
23 100 37 0 32,508 32,508 10.8
24 100 46 0 27,864 27,864 9.3
25 100 55 0 23,220 23,220 7.7
26 100 64 0 18,576 18,576 6.2
27 100 73 0 13,932 13,932 4.6
28 100 82 0 9,288 9,288 3.1
29 100 91 0 4,644 4,644 1.5
30 100 100 0 0 0 0.0

Key Statistics

cat("\n=== TAX ANALYSIS SUMMARY ===\n\n")
## 
## === TAX ANALYSIS SUMMARY ===
cat("Capital Gain (Nominal, NOT inflation-adjusted): €", 
    format(capital_gain, big.mark = ","), "\n\n")
## Capital Gain (Nominal, NOT inflation-adjusted): € 300,000
cat("At 12 Years Holding:\n")
## At 12 Years Holding:
cat("  IR Allowance: ", df$IR_Allowance_Pct[df$Years_Held == 12], "%\n")
##   IR Allowance:  42 %
cat("  PS Allowance: ", round(df$PS_Allowance_Pct[df$Years_Held == 12], 2), "%\n")
##   PS Allowance:  11.55 %
cat("  Total Tax: €", format(round(df$Total_Tax[df$Years_Held == 12]), big.mark = ","), "\n")
##   Total Tax: € 78,700
cat("  Effective Rate: ", round(df$Effective_Rate[df$Years_Held == 12], 2), "%\n\n")
##   Effective Rate:  26.23 %
cat("At 22 Years Holding (IR exemption achieved):\n")
## At 22 Years Holding (IR exemption achieved):
cat("  IR Allowance: ", df$IR_Allowance_Pct[df$Years_Held == 22], "%\n")
##   IR Allowance:  100 %
cat("  PS Allowance: ", round(df$PS_Allowance_Pct[df$Years_Held == 22], 2), "%\n")
##   PS Allowance:  28 %
cat("  Total Tax: €", format(round(df$Total_Tax[df$Years_Held == 22]), big.mark = ","), "\n")
##   Total Tax: € 37,152
cat("  Effective Rate: ", round(df$Effective_Rate[df$Years_Held == 22], 2), "%\n\n")
##   Effective Rate:  12.38 %
cat("At 30 Years Holding (Full exemption):\n")
## At 30 Years Holding (Full exemption):
cat("  IR Allowance: ", df$IR_Allowance_Pct[df$Years_Held == 30], "%\n")
##   IR Allowance:  100 %
cat("  PS Allowance: ", round(df$PS_Allowance_Pct[df$Years_Held == 30], 2), "%\n")
##   PS Allowance:  100 %
cat("  Total Tax: €", format(round(df$Total_Tax[df$Years_Held == 30]), big.mark = ","), "\n")
##   Total Tax: € 0
cat("  Effective Rate: ", round(df$Effective_Rate[df$Years_Held == 30], 2), "%\n\n")
##   Effective Rate:  0 %
cat("Tax Savings (12 vs 30 years): €", 
    format(round(max(df$Total_Tax) - min(df$Total_Tax)), big.mark = ","), "\n")
## Tax Savings (12 vs 30 years): € 78,700

Visualizations

Total Tax Payable Over Time

ggplot(df, aes(x = Years_Held, y = Total_Tax)) +
  geom_line(color = 'red', size = 1.2) +
  geom_point(shape = 23, fill = 'red', color = 'darkred', size = 3) +
  geom_ribbon(aes(ymin = 0, ymax = Total_Tax), fill = 'red', alpha = 0.1) +
  labs(
    title = 'Total French Tax Payable on German Property Sale',
    subtitle = 'Based on €300k nominal gain (NO inflation adjustment to cost base)\nRates from impots.gouv.fr - IR: 19%, PS: 17.2%',
    x = 'Years of Holding',
    y = 'Tax Amount (€)',
    caption = 'Source: impots.gouv.fr (July 2022)'
  ) +
  scale_y_continuous(labels = comma) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = 'bold', size = 14),
    plot.subtitle = element_text(size = 10, color = 'gray40'),
    plot.caption = element_text(size = 8, color = 'gray50', hjust = 0),
    panel.grid.minor = element_blank()
  )

ggsave('total_tax_payable.png', width = 10, height = 6, dpi = 300)

Tax Components Breakdown

# Reshape data for stacked visualization
df_long <- df %>%
  select(Years_Held, Income_Tax, Social_Levies) %>%
  tidyr::pivot_longer(cols = c(Income_Tax, Social_Levies),
                      names_to = "Tax_Type",
                      values_to = "Amount")

ggplot(df_long, aes(x = Years_Held, y = Amount, fill = Tax_Type)) +
  geom_area(alpha = 0.7) +
  scale_fill_manual(
    values = c("Income_Tax" = "#E74C3C", "Social_Levies" = "#3498DB"),
    labels = c("Income Tax - IR (19%)", "Social Levies - PS (17.2%)")
  ) +
  labs(
    title = 'Tax Components Over Holding Period',
    subtitle = 'Income Tax (IR) fully exempt at 22 years | Social Levies (PS) fully exempt at 30 years',
    x = 'Years of Holding',
    y = 'Tax Amount (€)',
    fill = 'Tax Component',
    caption = 'Allowance rates from impots.gouv.fr: IR 6%/yr (yrs 6-21), PS 1.65%/yr (yrs 6-21) + 9%/yr (yrs 23-30)'
  ) +
  scale_y_continuous(labels = comma) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = 'bold', size = 14),
    plot.caption = element_text(size = 8, color = 'gray50', hjust = 0),
    legend.position = 'bottom'
  )

Effective Tax Rate Trend

ggplot(df, aes(x = Years_Held, y = Effective_Rate)) +
  geom_line(color = '#2C3E50', size = 1.2) +
  geom_point(color = '#2C3E50', size = 2.5) +
  geom_hline(yintercept = 0, linetype = 'dashed', color = 'red') +
  geom_vline(xintercept = 22, linetype = 'dotted', color = 'blue', alpha = 0.5) +
  geom_vline(xintercept = 30, linetype = 'dotted', color = 'darkgreen', alpha = 0.5) +
  annotate("text", x = 22, y = 15, label = "IR Exempt\n(22 yrs)", 
           color = 'blue', size = 3, hjust = -0.1) +
  annotate("text", x = 30, y = 5, label = "Fully Exempt\n(30 yrs)", 
           color = 'darkgreen', size = 3, hjust = -0.1) +
  labs(
    title = 'Effective Tax Rate on Capital Gain',
    subtitle = 'Percentage of nominal €300k gain paid in taxes (no inflation adjustment)',
    x = 'Years of Holding',
    y = 'Effective Tax Rate (%)',
    caption = 'Source: Official allowances from impots.gouv.fr'
  ) +
  scale_y_continuous(breaks = seq(0, 40, 5)) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = 'bold', size = 14),
    plot.caption = element_text(size = 8, color = 'gray50', hjust = 0)
  )

Allowance Accumulation Over Time

ggplot(df, aes(x = Years_Held)) +
  geom_line(aes(y = IR_Allowance_Pct, color = "Income Tax (IR)"), size = 1.2) +
  geom_line(aes(y = PS_Allowance_Pct, color = "Social Levies (PS)"), size = 1.2) +
  geom_point(aes(y = IR_Allowance_Pct, color = "Income Tax (IR)"), size = 2) +
  geom_point(aes(y = PS_Allowance_Pct, color = "Social Levies (PS)"), size = 2) +
  scale_color_manual(
    values = c("Income Tax (IR)" = "#E74C3C", "Social Levies (PS)" = "#3498DB"),
    name = "Tax Type"
  ) +
  labs(
    title = 'Cumulative Tax Allowances by Holding Period',
    subtitle = 'Progression of official allowances from impots.gouv.fr',
    x = 'Years of Holding',
    y = 'Allowance Percentage (%)',
    caption = 'IR: 6%/yr (yrs 6-21) + 4% (yr 22) = 100% | PS: 1.65%/yr (yrs 6-21) + 1.6% (yr 22) + 9%/yr (yrs 23-30) = 100%'
  ) +
  scale_y_continuous(breaks = seq(0, 100, 10), limits = c(0, 105)) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = 'bold', size = 14),
    plot.caption = element_text(size = 8, color = 'gray50', hjust = 0),
    legend.position = 'bottom'
  )


Interpretation & Key Findings

Main Conclusions

  1. No Inflation Adjustment: The cost base of €250,000 remains fixed at the original purchase price. The entire €300,000 difference is treated as taxable gain, with no adjustment for inflation over the holding period. This is consistent with French tax law (Code Général des Impôts).

  2. Time-Based Relief: While there’s no inflation indexation, France provides generous time-based allowances that effectively reduce the tax burden on long-held properties according to official rates published by impots.gouv.fr.

  3. Two-Tier Exemption System (per impots.gouv.fr):

    • Income tax (19% rate) is fully exempt after 22 years
    • Social levies (17.2% rate) are fully exempt after 30 years
  4. Maximum Tax Burden: At 12 years of holding, the total tax is €78,700 (effective rate: 26.2%)

  5. Minimum Tax Burden: At 30 years of holding, the total tax drops to €0 (effective rate: 0%)

  6. Tax Savings Through Holding: Holding the property for 30 years instead of 12 years saves €78,700 in taxes.

Verification of Calculations

To verify the accuracy of our implementation against official sources:

At 12 years holding: - IR Allowance: (12-5) × 6% = 42% - PS Allowance: (12-5) × 1.65% = 11.55% - Matches official formula from impots.gouv.fr ✓

At 22 years holding: - IR Allowance: (21-5) × 6% + 4% = 96% + 4% = 100% - PS Allowance: (21-5) × 1.65% + 1.6% = 26.4% + 1.6% = 28% - Matches official formula from impots.gouv.fr ✓

At 30 years holding: - IR Allowance: 100% (achieved at year 22) - PS Allowance: 28% + (30-22) × 9% = 28% + 72% = 100% - Matches official formula from impots.gouv.fr ✓

Important Distinction

Inflation Adjustment vs. Time-Based Allowances:

  • Inflation adjustment would increase the cost base each year based on a price index (e.g., CPI), reducing the nominal capital gain. This is used in countries like the UK for certain assets.

  • Time-based allowances (French system) reduce the percentage of the nominal gain that is taxable based on holding period, but do not change the gain calculation itself.

France uses the latter approach for real estate capital gains taxation, as confirmed by impots.gouv.fr.

Additional Tax Considerations

According to official sources, additional taxes may apply:

  • Surtax on large gains (Article 1609 nonies G CGI): Progressive surtax of 2-6% on gains exceeding €50,000
  • Improvement allowance: Alternative flat-rate 15% allowance for properties held >5 years (instead of actual costs)

These additional elements are not included in this analysis but should be considered for complete tax planning.


References and Sources

Primary Sources

  1. French Tax Authority (Direction Générale des Finances Publiques)
  2. Code Général des Impôts (French Tax Code)
    • Article 150 U: Capital gains exemptions
    • Article 150 VC: Time-based allowances for income tax
    • Article 150 VB II: Alternative improvement cost allowances

Secondary Sources

  1. Notaires de France
  2. PwC Worldwide Tax Summaries

Note on Data Reliability

All calculations in this document are based on official rates and formulas published by the French government (impots.gouv.fr). Tax laws may change; users should verify current rates with official sources or qualified tax advisors before making financial decisions.


Session Information

sessionInfo()
## R version 4.5.1 (2025-06-13)
## Platform: aarch64-apple-darwin20
## Running under: macOS Tahoe 26.2
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: Europe/Paris
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] scales_1.4.0  knitr_1.50    dplyr_1.1.4   ggplot2_4.0.1
## 
## loaded via a namespace (and not attached):
##  [1] vctrs_0.6.5        cli_3.6.5          rlang_1.1.6        xfun_0.53         
##  [5] purrr_1.2.0        generics_0.1.4     textshaping_1.0.1  S7_0.2.0          
##  [9] jsonlite_2.0.0     labeling_0.4.3     glue_1.8.0         htmltools_0.5.8.1 
## [13] ragg_1.4.0         sass_0.4.10        rmarkdown_2.30     grid_4.5.1        
## [17] tibble_3.3.0       evaluate_1.0.5     jquerylib_0.1.4    fastmap_1.2.0     
## [21] yaml_2.3.10        lifecycle_1.0.4    compiler_4.5.1     RColorBrewer_1.1-3
## [25] pkgconfig_2.0.3    tidyr_1.3.1        rstudioapi_0.17.1  systemfonts_1.2.3 
## [29] farver_2.1.2       digest_0.6.37      R6_2.6.1           tidyselect_1.2.1  
## [33] pillar_1.11.1      magrittr_2.0.4     bslib_0.9.0        withr_3.0.2       
## [37] tools_4.5.1        gtable_0.3.6       cachem_1.1.0