Data cleaning and preparation skill 04 of 7

Data type conversion

Numbers stored as text do not add up, a “False” that is really a string filters as true, and an ID cast to a number loses its leading zero. This skill makes an AI agent convert each column to the type its job needs, handle the values that will not convert, keep missing data intact, and prove the result afterwards.

handling-missing-data/SKILL.md, version 1.0.0, about 2,900 words.

Download .mdSkill folder (.zip)

1. When to use this skill, and when not to

Use this skill when a column holds the right values in the wrong type, and you need to change the type before analysis, joining, sorting or modelling. The common signs are arithmetic that concatenates instead of adding, a sort that puts "100" before "20", a filter on True that returns everything, a group-by that treats "Yes" and "yes " as two groups, or a file that uses far more memory than it should.

Use it for the decision, not just the one-line cast. The point of the skill is to pick the safe conversion, handle the values that will not convert, keep missing values intact and prove afterwards that nothing changed by accident.

Do not use it for four neighbouring jobs. Parsing dates and times out of messy strings belongs in date-parsing, because dates have their own ambiguity and timezone rules. Standardising inconsistent text ("USA", "U.S.", "United States") belongs in text-standardisation; do that first, then convert the cleaned column. Deciding what to do about the gaps themselves belongs in handling-missing-data. Designing the nullability and types of a database schema is a modelling task, not a cleaning one.

2. Inputs the agent must have before starting

Ask for anything on this list that is missing. Do not convert on assumption.

  • The tool and version. pandas 1.x and 2.x differ on nullable types and on errors="ignore". Say which is in use.
  • The current dtype of each column to convert, and the type it should become. "Should be an integer" and "should be a whole number that can be missing" lead to different casts.
  • The real meaning of a non-numeric token. A blank, "N/A", "-", "unknown" or "-999" in a number column may be a missing value, not a parse error. Confirm before coercing it to NaN.
  • For money and identifiers, the precision rule. Account numbers, phone numbers and product codes that start with zero must stay text. Currency needs a fixed-precision plan.
  • Whether rows that fail to convert should be dropped, flagged or investigated. The default is to flag and report, never to drop silently.

If the dataset is unlabelled, profile first: print the dtype, three sample values and the count of values that fail a trial conversion for each target column, then confirm the plan.

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as data-type-conversion/SKILL.md.

---
name: data-type-conversion
description: Convert columns to the correct, intended data type so numbers compute, dates sort, booleans filter and categories group, across pandas, SQL and R. Covers columns read as text that should be numeric, the astype versus to_numeric choice, errors="coerce", nullable integers (Int64) when NaN is present, the boolean-from-string trap where "False" becomes True, category dtype for low-cardinality columns, downcasting for memory, float precision for money, and verifying every conversion by counting what failed. Use it when someone says "convert this column", "it's stored as text", "astype", "to_numeric", "cannot convert float NaN to integer", "change the dtype", "my numbers won't add up", "True/False is a string", or "shrink the memory". Do NOT use it for parsing messy date and time strings (see date-parsing), for standardising inconsistent text values before you map them (see text-standardisation), for filling the gaps themselves (see handling-missing-data), or for designing a database schema.
license: Free to use and adapt with credit to Afeez Adebayo (https://afeezadebayo.com). Link back to this skill at https://afeezadebayo.com/skills-md/data/data-cleaning-preparation/data-type-conversion/ and to https://www.linkedin.com/in/afeez-adebayo/.
---

# Data type conversion

## 1. When to use this skill, and when not to

Use this skill when a column holds the right values in the wrong type, and you need to change the type before analysis, joining, sorting or modelling. The common signs are arithmetic that concatenates instead of adding, a sort that puts "100" before "20", a filter on True that returns everything, a group-by that treats "Yes" and "yes " as two groups, or a file that uses far more memory than it should.

Use it for the decision, not just the one-line cast. The point of the skill is to pick the safe conversion, handle the values that will not convert, keep missing values intact and prove afterwards that nothing changed by accident.

Do not use it for four neighbouring jobs. Parsing dates and times out of messy strings belongs in date-parsing, because dates have their own ambiguity and timezone rules. Standardising inconsistent text ("USA", "U.S.", "United States") belongs in text-standardisation; do that first, then convert the cleaned column. Deciding what to do about the gaps themselves belongs in handling-missing-data. Designing the nullability and types of a database schema is a modelling task, not a cleaning one.

## 2. Inputs the agent must have before starting

Ask for anything on this list that is missing. Do not convert on assumption.

- The tool and version. pandas 1.x and 2.x differ on nullable types and on `errors="ignore"`. Say which is in use.
- The current dtype of each column to convert, and the type it should become. "Should be an integer" and "should be a whole number that can be missing" lead to different casts.
- The real meaning of a non-numeric token. A blank, "N/A", "-", "unknown" or "-999" in a number column may be a missing value, not a parse error. Confirm before coercing it to NaN.
- For money and identifiers, the precision rule. Account numbers, phone numbers and product codes that start with zero must stay text. Currency needs a fixed-precision plan.
- Whether rows that fail to convert should be dropped, flagged or investigated. The default is to flag and report, never to drop silently.

If the dataset is unlabelled, profile first: print the dtype, three sample values and the count of values that fail a trial conversion for each target column, then confirm the plan.

## 3. The conversion workflow, step by step

1. Profile the column before touching it. Record the current dtype, the number of rows, the number of distinct values, and the first few values. A column of numbers stored as text almost always hides a stray character: a thousands separator, a currency symbol, a trailing space or a footnote mark.

2. Decide the target type from the column's job, not its current appearance.
   - If the values are measured quantities you will compute with, target a numeric type (integer or float).
   - If a "number" is really a label you will never add up (postcode, ID, phone, year-as-category), keep it as text or category.
   - If the column is a flag, target boolean.
   - If the column has fewer than roughly half as many distinct values as rows, and the values repeat, consider the category type for memory and speed.
   - If the column is a date or time, stop and use date-parsing.

3. Clean the blockers before casting, not during. Strip whitespace, remove thousands separators, currency symbols and percent signs, and map the agreed missing tokens to a true null. Keep this as a separate, inspectable step so you can see what you removed.

4. Choose the conversion function by how forgiving you need to be.
   - For a direct, all-or-nothing cast where every value is already clean, use the native cast (`astype` in pandas, `CAST` in SQL, `as.numeric` in R).
   - For a column that may contain values that cannot convert, use the coercing path (`to_numeric(errors="coerce")` in pandas, `TRY_CAST` or `SAFE_CAST` in SQL) so bad values become null instead of raising, then count them.

5. Handle the integer-with-missing case explicitly. A plain integer type cannot hold a missing value. In pandas, `astype(int)` on a column with any NaN raises "Cannot convert non-finite values (NA or inf) to integer", and `to_numeric(errors="coerce")` returns float64 even when every value is a whole number, because NaN is a float. If the column is conceptually an integer that can be missing, convert to the nullable integer type `Int64` (capital I), not `int`. Do not reach for `fillna(0)` to force the cast, because zero is a real value and inventing it distorts counts and averages.

6. Convert booleans by an explicit map, never by a blind cast. `astype(bool)` on text treats every non-empty string as True, so the string "False" becomes True and "0" becomes True. Build a dictionary ({"true": True, "yes": True, "1": True, "false": False, "no": False, "0": False}, applied to the lower-cased, stripped value) and map against it. Any value not in the map should become null and be reported, not guessed.

7. Protect precision for money and large numbers. Never store currency as a binary float, because 0.1 plus 0.2 is not 0.3 in float arithmetic and the error compounds over a sum. Store money as integer minor units (pence, cents) or as a fixed-precision decimal type. Very large integers beyond the 64-bit range will silently become floats and lose their last digits; keep them as strings unless you need to compute with them.

8. Apply the smallest type that fits, only when memory is the goal. Downcast a clean numeric column with the integer or float downcast option, or move a repetitive text column to the category type. Measure memory before and after so the saving is real; do not downcast by reflex, because it can cost precision and interferes with later joins.

9. Order the categories when the values have a rank. A plain category treats "small", "medium" and "large" as unordered labels, so a comparison like `size > "medium"` fails and a sort is alphabetical. When a column is ordinal, declare the order explicitly (an ordered `CategoricalDtype` in pandas, an ordered `factor` with `levels=` in R) so comparison and sorting follow the real sequence rather than the alphabet. Leave nominal categories (region, colour) unordered.

10. Prefer vectorised conversion over row-by-row work. A column-wide cast or `to_numeric` runs in one compiled pass; a per-row `apply` with a Python function over the same column can be ten to a hundred times slower and is easy to get subtly wrong. Reach for `apply` only when no vectorised method exists, and even then convert the column once rather than inside a loop.

11. Verify every conversion and report. For each converted column, confirm the new dtype, count how many values became null during the conversion, and compare that count against the number of recognised missing tokens you expected. If more values failed than you expected, stop and show examples. For a high-stakes column, run a round-trip check: cast the new column back to text and compare it against the cleaned original, so a silent truncation or rounding shows up as a mismatch. State the summary in plain terms: "price converted from text to Int64, 14 of 10,000 values could not parse and are now null, all 14 contained the footnote mark '*'."

## 4. Worked example, end to end

A sales export has a `price` column read as text, a `vip` column of flags, and a `region` column that repeats across 50,000 rows.

The obvious move, `df["price"].astype(int)`, raises immediately because some cells are blank and one is "1,299". The careful route is to profile first: `price` is object dtype, 50,000 rows, with values like "1,299", "899.00", "", and "N/A". So clean, then coerce, then type for missingness:

```python
cleaned = (df["price"].astype("string")
           .str.replace(",", "", regex=False)
           .str.strip()
           .replace({"N/A": pd.NA, "": pd.NA}))
df["price"] = pd.to_numeric(cleaned, errors="coerce")
failed = df["price"].isna().sum()
```

`failed` is 37. The 37 are the blanks and "N/A" values we expected, so this is missingness, not a parse error. Because price is money, we do not keep it as a float with rounding risk; we store pence as a nullable integer:

```python
df["price_pence"] = (df["price"] * 100).round().astype("Int64")
```

For `vip`, a blind `astype(bool)` would mark the 1,200 rows that literally say "False" as True. Instead:

```python
truth = {"yes": True, "true": True, "1": True, "y": True,
         "no": False, "false": False, "0": False, "n": False}
key = df["vip"].astype("string").str.strip().str.lower()
df["vip"] = key.map(truth).astype("boolean")
unmapped = key[~key.isin(truth) & key.notna()].unique()
```

`unmapped` returns `["maybe"]`, a value nobody anticipated. We report it rather than coercing it. For `region`, the category type cuts memory because 50,000 rows hold only six distinct regions:

```python
before = df["region"].memory_usage(deep=True)
df["region"] = df["region"].astype("category")
after = df["region"].memory_usage(deep=True)
```

Memory drops from about 3.1 MB to about 50 KB. The handover states each change, the failure counts, and the one value ("maybe") that needs a human decision.

## 5. Templates you can use verbatim

Profiling prompt:

> Before converting, print for each target column: current dtype, row count, distinct count, five sample values, and the number of values that fail a trial `to_numeric(errors="coerce")`. Do not convert yet. Tell me which columns look like IDs or money that should stay text.

Safe numeric conversion (pandas):

```python
s = (df[col].astype("string").str.strip()
     .str.replace(r"[,£$%]", "", regex=True)
     .replace({"N/A": pd.NA, "-": pd.NA, "": pd.NA}))
out = pd.to_numeric(s, errors="coerce")
# integer that can be missing:
out = out.astype("Int64") if (out.dropna() % 1 == 0).all() else out
```

Safe conversion (SQL):

```sql
SELECT id,
       SAFE_CAST(REPLACE(price, ',', '') AS NUMERIC) AS price_num
FROM sales;  -- BigQuery; use TRY_CAST in SQL Server, TRY_CONVERT elsewhere
```

Post-conversion report:

> For each converted column report: old dtype, new dtype, count of values coerced to null, and whether that equals the expected count of missing tokens. List any value that could not be mapped.

## 6. Failure modes and anti-patterns

- Forcing an integer cast through a missing value with `fillna(0)`. The rows convert, but zero is now mixed in with real quantities and every count and average is wrong. Spot it when a total is suspiciously round or a minimum is zero. Use `Int64` instead and keep the null.
- Casting a boolean from text with `astype(bool)`. Every non-empty string becomes True, so "False", "no" and "0" all read as True. Spot it when almost every row is True. Map explicitly against a truth dictionary.
- Storing money as float. Sums drift by fractions of a penny and reconciliations fail. Spot it when a total disagrees with the source by a tiny amount. Store minor units as integers or use a decimal type.
- Letting `to_numeric(errors="coerce")` run without counting the nulls it created. Bad values vanish into NaN and the row survives looking clean. Always compare the new null count against what you expected, and show examples when they differ.
- Converting an ID or postcode to a number. Leading zeros disappear ("007" becomes 7) and long codes lose their last digits past the 64-bit limit. Spot it when codes are shorter than they should be. Keep identifiers as text.

## 7. Quality bar before handover

- Every converted column has its old dtype, new dtype and coerced-null count reported.
- No conversion used `fillna` to force an integer cast; integers that can be missing use a nullable type.
- Booleans were mapped from an explicit dictionary, and any unmapped value is listed, not guessed.
- Money is stored as integer minor units or a decimal type, never a binary float.
- Identifiers, postcodes and phone numbers remain text, with leading zeros intact.
- Any downcasting or category conversion names the memory before and after.
- The original column is preserved, or the change is reversible, until the person confirms the result.

## 8. Guardrails

- Irreversible steps need confirmation. Overwriting a source column, dropping rows that failed to convert, or saving over the input file are bulk edits; default to writing a new column or a new file and ask before replacing the original.
- Never invent values to make a cast succeed. A value that will not convert is either a missing value to null out and count, or a real value the plan missed; it is never a silent zero.
- Do not fabricate tool behaviour or version limits. Where behaviour is version-dependent (nullable types, `errors="ignore"` removal), verify against the installed version before relying on it.
- PII and credentials: ID numbers, card numbers and phone numbers often arrive in columns people want to "convert to numbers". Keep them as text, never log their values, and never paste them into an external service to clean them.
- This skill changes types, not meaning. It will not decide business rules (what counts as VIP, which currency a column is in); it surfaces those for a human.
- Cost: type conversion runs locally and spends no API credits or tokens on the data itself. The cheaper and faster path is always the vectorised cast over a per-row `apply`; if a file is too large to hold in memory, convert in chunks or push the cast into the database with `TRY_CAST` rather than loading everything.

## 9. Tool-specific appendix (pandas)

Fenced off from the tool-agnostic core above.

- `astype` is a strict, all-or-nothing cast; one bad value raises. `to_numeric` with `errors="coerce"` is the forgiving path and turns failures into NaN. `errors="ignore"` returned the input unchanged and is removed in pandas 3.0; do not rely on it.
- `pd.to_numeric` returns float64 when any value is missing, because NaN is a float. To keep whole numbers, chain `.astype("Int64")`, the NumPy-nullable integer, which holds `pd.NA`.
- `convert_dtypes()` infers the best nullable dtype per column and is a reasonable first pass, but check its choices; it can pick a wider type than you want.
- `downcast="integer"` or `downcast="float"` on `to_numeric` shrinks a clean numeric column to the smallest safe dtype. `dtype_backend="numpy_nullable"` or `"pyarrow"` (pandas 2.0+) returns nullable results where coerced failures become `pd.NA` rather than `NaN`.
- The `category` dtype stores repeated strings as integer codes against a lookup. It saves memory on low-cardinality columns and speeds group-by, but adding a value outside the known categories needs `cat.add_categories` first.
- For booleans use the nullable `"boolean"` dtype so a missing flag stays missing rather than defaulting to False.
- SQL equivalents: `TRY_CAST` / `TRY_CONVERT` (SQL Server), `SAFE_CAST` (BigQuery), `::numeric` with a cleaning step (PostgreSQL). R equivalents: `as.numeric`, `as.integer`, `as.logical` (map explicitly, as `as.logical("False")` is NA, not FALSE), and `factor` for categories.

## Sources

- pandas, pandas.to_numeric API reference, last verified 1 November 2026: https://pandas.pydata.org/docs/reference/api/pandas.to_numeric.html
- pandas, pandas.to_datetime API reference (for the numeric-with-format deprecation and nullable behaviour), last verified 1 November 2026: https://pandas.pydata.org/docs/reference/api/pandas.to_datetime.html
- GeeksforGeeks, How to Fix: ValueError: cannot convert float NaN to integer, last verified 1 November 2026: https://www.geeksforgeeks.org/
- Anthropic, Equipping agents for the real world with Agent Skills, last verified 1 November 2026: https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills
- Kanaries, pandas string operations reference, last verified 1 November 2026: https://docs.kanaries.net/topics/Pandas/pandas-string-operations

3. The conversion workflow, step by step

  1. Profile the column before touching it. Record the current dtype, the number of rows, the number of distinct values, and the first few values. A column of numbers stored as text almost always hides a stray character: a thousands separator, a currency symbol, a trailing space or a footnote mark.
  1. Decide the target type from the column's job, not its current appearance.
    • If the values are measured quantities you will compute with, target a numeric type (integer or float).
    • If a "number" is really a label you will never add up (postcode, ID, phone, year-as-category), keep it as text or category.
    • If the column is a flag, target boolean.
    • If the column has fewer than roughly half as many distinct values as rows, and the values repeat, consider the category type for memory and speed.
    • If the column is a date or time, stop and use date-parsing.
  1. Clean the blockers before casting, not during. Strip whitespace, remove thousands separators, currency symbols and percent signs, and map the agreed missing tokens to a true null. Keep this as a separate, inspectable step so you can see what you removed.
  1. Choose the conversion function by how forgiving you need to be.
    • For a direct, all-or-nothing cast where every value is already clean, use the native cast (astype in pandas, CAST in SQL, as.numeric in R).
    • For a column that may contain values that cannot convert, use the coercing path (to_numeric(errors="coerce") in pandas, TRY_CAST or SAFE_CAST in SQL) so bad values become null instead of raising, then count them.
  1. Handle the integer-with-missing case explicitly. A plain integer type cannot hold a missing value. In pandas, astype(int) on a column with any NaN raises "Cannot convert non-finite values (NA or inf) to integer", and to_numeric(errors="coerce") returns float64 even when every value is a whole number, because NaN is a float. If the column is conceptually an integer that can be missing, convert to the nullable integer type Int64 (capital I), not int. Do not reach for fillna(0) to force the cast, because zero is a real value and inventing it distorts counts and averages.
  1. Convert booleans by an explicit map, never by a blind cast. astype(bool) on text treats every non-empty string as True, so the string "False" becomes True and "0" becomes True. Build a dictionary ({"true": True, "yes": True, "1": True, "false": False, "no": False, "0": False}, applied to the lower-cased, stripped value) and map against it. Any value not in the map should become null and be reported, not guessed.
  1. Protect precision for money and large numbers. Never store currency as a binary float, because 0.1 plus 0.2 is not 0.3 in float arithmetic and the error compounds over a sum. Store money as integer minor units (pence, cents) or as a fixed-precision decimal type. Very large integers beyond the 64-bit range will silently become floats and lose their last digits; keep them as strings unless you need to compute with them.
  1. Apply the smallest type that fits, only when memory is the goal. Downcast a clean numeric column with the integer or float downcast option, or move a repetitive text column to the category type. Measure memory before and after so the saving is real; do not downcast by reflex, because it can cost precision and interferes with later joins.
  1. Order the categories when the values have a rank. A plain category treats "small", "medium" and "large" as unordered labels, so a comparison like size > "medium" fails and a sort is alphabetical. When a column is ordinal, declare the order explicitly (an ordered CategoricalDtype in pandas, an ordered factor with levels= in R) so comparison and sorting follow the real sequence rather than the alphabet. Leave nominal categories (region, colour) unordered.
  1. Prefer vectorised conversion over row-by-row work. A column-wide cast or to_numeric runs in one compiled pass; a per-row apply with a Python function over the same column can be ten to a hundred times slower and is easy to get subtly wrong. Reach for apply only when no vectorised method exists, and even then convert the column once rather than inside a loop.
  1. Verify every conversion and report. For each converted column, confirm the new dtype, count how many values became null during the conversion, and compare that count against the number of recognised missing tokens you expected. If more values failed than you expected, stop and show examples. For a high-stakes column, run a round-trip check: cast the new column back to text and compare it against the cleaned original, so a silent truncation or rounding shows up as a mismatch. State the summary in plain terms: "price converted from text to Int64, 14 of 10,000 values could not parse and are now null, all 14 contained the footnote mark '*'."

4. Worked example, end to end

A sales export has a price column read as text, a vip column of flags, and a region column that repeats across 50,000 rows.

The obvious move, df["price"].astype(int), raises immediately because some cells are blank and one is "1,299". The careful route is to profile first: price is object dtype, 50,000 rows, with values like "1,299", "899.00", "", and "N/A". So clean, then coerce, then type for missingness:

cleaned = (df["price"].astype("string")
           .str.replace(",", "", regex=False)
           .str.strip()
           .replace({"N/A": pd.NA, "": pd.NA}))
df["price"] = pd.to_numeric(cleaned, errors="coerce")
failed = df["price"].isna().sum()

failed is 37. The 37 are the blanks and "N/A" values we expected, so this is missingness, not a parse error. Because price is money, we do not keep it as a float with rounding risk; we store pence as a nullable integer:

df["price_pence"] = (df["price"] * 100).round().astype("Int64")

For vip, a blind astype(bool) would mark the 1,200 rows that literally say "False" as True. Instead:

truth = {"yes": True, "true": True, "1": True, "y": True,
         "no": False, "false": False, "0": False, "n": False}
key = df["vip"].astype("string").str.strip().str.lower()
df["vip"] = key.map(truth).astype("boolean")
unmapped = key[~key.isin(truth) & key.notna()].unique()

unmapped returns ["maybe"], a value nobody anticipated. We report it rather than coercing it. For region, the category type cuts memory because 50,000 rows hold only six distinct regions:

before = df["region"].memory_usage(deep=True)
df["region"] = df["region"].astype("category")
after = df["region"].memory_usage(deep=True)

Memory drops from about 3.1 MB to about 50 KB. The handover states each change, the failure counts, and the one value ("maybe") that needs a human decision.

5. Templates you can use verbatim

Profiling prompt:

> Before converting, print for each target column: current dtype, row count, distinct count, five sample values, and the number of values that fail a trial to_numeric(errors="coerce"). Do not convert yet. Tell me which columns look like IDs or money that should stay text.

Safe numeric conversion (pandas):

s = (df[col].astype("string").str.strip()
     .str.replace(r"[,£$%]", "", regex=True)
     .replace({"N/A": pd.NA, "-": pd.NA, "": pd.NA}))
out = pd.to_numeric(s, errors="coerce")
# integer that can be missing:
out = out.astype("Int64") if (out.dropna() % 1 == 0).all() else out

Safe conversion (SQL):

SELECT id,
       SAFE_CAST(REPLACE(price, ',', '') AS NUMERIC) AS price_num
FROM sales;  -- BigQuery; use TRY_CAST in SQL Server, TRY_CONVERT elsewhere

Post-conversion report:

> For each converted column report: old dtype, new dtype, count of values coerced to null, and whether that equals the expected count of missing tokens. List any value that could not be mapped.

6. Failure modes and anti-patterns

  • Forcing an integer cast through a missing value with fillna(0). The rows convert, but zero is now mixed in with real quantities and every count and average is wrong. Spot it when a total is suspiciously round or a minimum is zero. Use Int64 instead and keep the null.
  • Casting a boolean from text with astype(bool). Every non-empty string becomes True, so "False", "no" and "0" all read as True. Spot it when almost every row is True. Map explicitly against a truth dictionary.
  • Storing money as float. Sums drift by fractions of a penny and reconciliations fail. Spot it when a total disagrees with the source by a tiny amount. Store minor units as integers or use a decimal type.
  • Letting to_numeric(errors="coerce") run without counting the nulls it created. Bad values vanish into NaN and the row survives looking clean. Always compare the new null count against what you expected, and show examples when they differ.
  • Converting an ID or postcode to a number. Leading zeros disappear ("007" becomes 7) and long codes lose their last digits past the 64-bit limit. Spot it when codes are shorter than they should be. Keep identifiers as text.

7. Quality bar before handover

  • Every converted column has its old dtype, new dtype and coerced-null count reported.
  • No conversion used fillna to force an integer cast; integers that can be missing use a nullable type.
  • Booleans were mapped from an explicit dictionary, and any unmapped value is listed, not guessed.
  • Money is stored as integer minor units or a decimal type, never a binary float.
  • Identifiers, postcodes and phone numbers remain text, with leading zeros intact.
  • Any downcasting or category conversion names the memory before and after.
  • The original column is preserved, or the change is reversible, until the person confirms the result.

8. Guardrails

  • Irreversible steps need confirmation. Overwriting a source column, dropping rows that failed to convert, or saving over the input file are bulk edits; default to writing a new column or a new file and ask before replacing the original.
  • Never invent values to make a cast succeed. A value that will not convert is either a missing value to null out and count, or a real value the plan missed; it is never a silent zero.
  • Do not fabricate tool behaviour or version limits. Where behaviour is version-dependent (nullable types, errors="ignore" removal), verify against the installed version before relying on it.
  • PII and credentials: ID numbers, card numbers and phone numbers often arrive in columns people want to "convert to numbers". Keep them as text, never log their values, and never paste them into an external service to clean them.
  • This skill changes types, not meaning. It will not decide business rules (what counts as VIP, which currency a column is in); it surfaces those for a human.
  • Cost: type conversion runs locally and spends no API credits or tokens on the data itself. The cheaper and faster path is always the vectorised cast over a per-row apply; if a file is too large to hold in memory, convert in chunks or push the cast into the database with TRY_CAST rather than loading everything.

9. Tool-specific appendix (pandas)

Fenced off from the tool-agnostic core above.

  • astype is a strict, all-or-nothing cast; one bad value raises. to_numeric with errors="coerce" is the forgiving path and turns failures into NaN. errors="ignore" returned the input unchanged and is removed in pandas 3.0; do not rely on it.
  • pd.to_numeric returns float64 when any value is missing, because NaN is a float. To keep whole numbers, chain .astype("Int64"), the NumPy-nullable integer, which holds pd.NA.
  • convert_dtypes() infers the best nullable dtype per column and is a reasonable first pass, but check its choices; it can pick a wider type than you want.
  • downcast="integer" or downcast="float" on to_numeric shrinks a clean numeric column to the smallest safe dtype. dtype_backend="numpy_nullable" or "pyarrow" (pandas 2.0+) returns nullable results where coerced failures become pd.NA rather than NaN.
  • The category dtype stores repeated strings as integer codes against a lookup. It saves memory on low-cardinality columns and speeds group-by, but adding a value outside the known categories needs cat.add_categories first.
  • For booleans use the nullable "boolean" dtype so a missing flag stays missing rather than defaulting to False.
  • SQL equivalents: TRY_CAST / TRY_CONVERT (SQL Server), SAFE_CAST (BigQuery), ::numeric with a cleaning step (PostgreSQL). R equivalents: as.numeric, as.integer, as.logical (map explicitly, as as.logical("False") is NA, not FALSE), and factor for categories.

Sources

When to use this skill, and when not to

Use it when a column holds the right values in the wrong type: prices stored as text, a year read as a float, a flag column of “Yes” and “No”, or a file that uses more memory than it should. It handles the conversion and the values that resist it.

It is not for parsing messy dates (see date-parsing), standardising inconsistent text (see text-standardisation), or deciding what to do about the gaps themselves (see handling-missing-data).

Who it is for

Analysts, data scientists and engineers, and anyone using an AI agent to prepare a dataset before a report, a join or a model. If you have seen “100” sort before “20”, or an average come out wrong because a number was secretly a string, this is the step you were missing.

What is inside

An eleven-step workflow with decision rules, not vague advice. It covers profiling a column before touching it, choosing the target type from the column’s job, cleaning blockers before casting, the astype versus to_numeric choice, nullable integers when values can be missing, mapping booleans from an explicit dictionary, protecting precision for money, ordered categories for ranked values, and verifying every conversion by counting what failed.

Example prompts

“Convert the price column to a number.” “Why does astype(int) say it cannot convert float NaN to integer?” “This True/False column is stored as text, fix it.” “Shrink the memory on this file.” “My postcodes lost their leading zeros.”

Example output

The skill returns the conversion with the reasoning stated, not just a cast. A shortened version: price was text with commas and “N/A”; cleaned, coerced and stored as pence in a nullable integer; 37 values became null, all of them the expected blanks and “N/A”; one flag value, “maybe”, could not be mapped and needs your decision.

What the agent needs from you

The tool and version, the current type of each column and the type it should become, the real meaning of a blank or a token like -999, the precision rule for money and identifiers, and whether rows that fail to convert should be flagged or investigated. If anything is missing, the skill profiles the column and asks rather than guessing.

How to install and use it

In the Claude apps, download the SKILL.md from this page, put it in a folder named data-type-conversion, and add it as a skill or upload it to a Project. In Claude Code or another agent, drop the folder into your skills directory. Then give the agent the dataset and say what each column should become.

How to convert data types step by step

Install the skill as data-type-conversion/SKILL.md. Give the agent the file, say which columns to convert and their intended types, and name any token that means “missing”. The skill profiles each column, cleans the blockers, picks astype or to_numeric by how forgiving it must be, keeps integers that can be missing as a nullable type, maps booleans explicitly, and reports the new type and the count of values that failed for every column it touched.

Worked example

A sales export has price as text (“1,299”, “”, “N/A”), a vip flag of words, and a region that repeats across 50,000 rows. The obvious astype(int) fails on the blanks. The skill cleans the commas and tokens, coerces to a number, stores price as pence in a nullable integer, maps vip against a truth dictionary (catching a stray “maybe”), and moves region to the category type, cutting its memory from about 3 MB to 50 KB. Every change is reported with its failure count.

Situations this skill handles

SituationWhat the skill does
Numbers stored as textStrips separators and symbols, then coerces and counts what fails
astype(int) fails on NaNConverts to the nullable Int64 type instead of inventing a zero
“False” stored as a stringMaps booleans from an explicit dictionary, not a blind cast
Money in a float columnStores integer minor units or a decimal type to protect precision
A repetitive text columnMoves it to the category type and measures the memory saved
An ID or postcode of digitsKeeps it as text so leading zeros and long codes survive

What makes this skill different

Most guides stop at astype. This skill adds the three things that go wrong in practice: it keeps integers that can be missing as a nullable type instead of forcing a zero, it maps booleans explicitly so “False” does not become true, and it counts the values that failed every conversion so nothing vanishes into a silent null. It also protects money and identifiers from the quiet precision loss of floats and numeric IDs.

Common mistakes

Forcing an integer cast with fillna(0) and corrupting every count. Casting a boolean from text so every non-empty string reads as true. Storing currency as a float and watching the totals drift. Running to_numeric with coerce and never counting the nulls it created. Converting an ID to a number and losing its leading zeros. The skill names each one and gives the safe move.

Run text-standardisation first when the values are inconsistent before you map them, and date-parsing when a column is really a date. Pair it with handling-missing-data to decide what the nulls mean once the type is right, and with duplicate-detection and outlier-detection as the rest of the cleaning pass.

Frequently asked questions

Use astype when every value is already clean and you want an all-or-nothing cast that raises on anything unexpected. Use to_numeric with errors=‘coerce’ when the column may hold values that cannot convert, so they become null and you can count them rather than hitting an exception.

A plain integer type cannot hold a missing value, and your column has at least one NaN. Do not fill it with zero to force the cast. Convert to the nullable integer type Int64, with a capital I, which keeps the gap as a real missing value.

astype(bool) treats every non-empty string as true, so the text “False” and “0” both become true. Map “true”, “yes” and “1” to true and “false”, “no” and “0” to false against the lower-cased value, and leave anything unmatched as null to report.

Not as a binary float, because 0.1 plus 0.2 is not 0.3 in float arithmetic and the error grows over a sum. Store currency as integer minor units, such as pence or cents, or as a fixed-precision decimal type, and format for display only at the end.

When a column has far fewer distinct values than rows and the values repeat, such as a region or status column. The category type stores the labels once as codes, which cuts memory and speeds group-by. Measure memory before and after, and order the categories when the values have a rank.

Because you converted it to a number, and a number has no leading zeros, so “007” becomes 7. Identifiers, postcodes and phone numbers are labels you never add up. Keep them as text, which also protects long codes from the precision loss that hits integers beyond the 64-bit range.

Changelog

1.0.0, 1 November 2026: first release. Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.