Have you upgraded to Pandas 3 and found that a simple assignment into a text column now fails?
places = pd.DataFrame({'city': ['Boston', 'New York'],
'zip': ['02134', '10001']})
places.loc[0, 'zip'] = 2134
TypeError: Invalid value '2134' for dtype 'str'. Value should be a string or missing value, got 'int' instead.
The short answer is that in Pandas 3, a column of text has a real string dtype, and it only accepts strings or missing values. Assign a string, and it works:
places.loc[0, 'zip'] = '02134'
Whether that's the right fix depends on why you were putting a number there, and in this case, the error saved you from a bug.
Text columns used to accept anything
Look at the column types:
places.dtypes
city str
zip str
dtype: object
In Pandas 2, those would have said object. An object column is a list of arbitrary Python objects, so it would take a string in one row, an integer in the next, and a dictionary in a third without complaint. Pandas 3 infers str for text instead, backed by PyArrow when it is installed. It's faster and uses less memory, and it makes a promise: every value in this column is text.
Assigning 2134 would break that promise, so Pandas refuses. It also refuses floats and booleans:
places.loc[0, 'zip'] = True
TypeError: Invalid value 'True' for dtype 'str'. Value should be a string or missing value, got 'bool' instead.
Missing values are fine, since None and np.nan are both stored as NaN.
Notice what the error prevented here. Boston's ZIP code is 02134. As an integer, it's 2134, and the leading zero is gone for good. The old object column would have accepted that without a word, and you would have found out when a mailing went to the wrong place.
If you meant the column to be numbers
Often the text column was never supposed to be text. Numbers read from a messy source, with some placeholder like "unknown," are stored as strings, and then the cleanup code tries to overwrite the placeholders with a number:
s = pd.Series(['3', 'unknown', '7'])
s[s == 'unknown'] = 0
TypeError: Invalid value '0' for dtype 'str'. Value should be a string or missing value, got 'int' instead.
Rather than patching values one by one, convert the column once, deliberately. pd.to_numeric with errors='coerce' turns anything that isn't a number into NaN, which is usually more honest than zero:
pd.to_numeric(s, errors='coerce')
0 3.0
1 NaN
2 7.0
dtype: float64
The same goes for recoding labels as numbers. Instead of assigning into the string column, build a new column with map:
scores = pd.Series(['high', 'low', 'high'])
scores.map({'high': 1, 'low': 0})
0 1
1 0
2 1
dtype: int64
Why replace works when assignment does not
Here's an inconsistency worth knowing about. Assignment refuses a number, but several methods accept one and quietly change the result's dtype:
zips = pd.Series(['02134', '10001'])
zips.replace('10001', 10001)
0 02134
1 10001
dtype: object
where, mask, and fillna behave the same way. The difference is that assignment modifies an existing column, which has made a promise about its contents. replace returns a brand-new series, so Pandas picks a type that can hold whatever you gave it, and that type is object.
So if your code stopped raising after you switched from assignment to replace, check the dtype. You may have traded a loud error for a mixed column of strings and integers, which is exactly what the str dtype was trying to prevent.
If you really do want mixed values
Occasionally a column genuinely should hold a mix. Say so explicitly by converting it to object first:
obj = s.astype(object)
obj[obj == 'unknown'] = 0
obj
0 3
1 0
2 7
dtype: object
That looks like a column of numbers, but the 3 and the 7 are still strings. Mixed columns are hard to reason about for exactly this reason, so it's worth doing only when you mean it.
Practice
See astype in Pandas for converting column types on real data, and to_numeric in Pandas for turning text into numbers safely. Then try the to_numeric exercise.