Repository: pydantic/pydantic
Pull request: Add private attributes support
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary
Private attributes declared as regular fields, but always start with underscore and `PrivateAttr` is used instead of `Field`. Upon class creation they added in `__slots__` and `Model.__private_attributes__` for faster lookups and support for default values
Default values declared just like fields: `_attr[: annotation] = default_value` or `_attr = PrivateAttr(default_value)`

## Example from docs
```py
from datetime import datetime
from random import randint

from pydantic import BaseModel, PrivateAttr


class TimeAwareModel(BaseModel):
    _processed_at: datetime = PrivateAttr(default_factory=datetime.now)
    _secret_value: str = PrivateAttr()

    def __init__(self, **data):
        super().__init__(**data)
        self._secret_value = randint(1, 5)


m = TimeAwareModel()
print(m._processed_at)
print(m._secret_value)

```

## Other changes
Replaced `copy.deepcopy()` usage with new `pydantic.utils.smart_deepcopy()` for faster immutable values handling

## Related issue number
Closes #655

## Checklist
* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Problem statement:
Question: add private attribute
I have a use case that I'd to add an attribute when initialising the instance which is not part of the model, thus should not be validated. Is that possible?

Here's a practical example:

```py
from pydantic import BaseModel
from datetime import datetime

class Test(BaseModel):
    a: int

class TestExtra(BaseModel):
    a: int

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._processed_at = datetime.utcnow()

test = {"a": 1}
Test(**test)
TestExtra(**test)  # ValueError: "TestExtra" object has no field "_processed_at"
```

Hints from issue discussion:
Yes, use underscore or `ClassVar`.
This works, but it's not what I want, as every instance will have the same timestamp:
```py
class TestExtra(BaseModel):
    a: int
    _procesed_at: datetime.utcnow()
```

This is what I want, but it fails with the same error:
```py
class TestExtra(BaseModel):
    a: int
    _processed_at: datetime

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._processed_at = datetime.utcnow()

TestExtra(a=1) 
```

Or am I missing something?
@samuelcolvin can this be reopened and solved please?
This should work:

```py
from datetime import datetime

from pydantic import BaseModel


class Test(BaseModel):
    a: int


class TestExtra(BaseModel):
    a: int

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        object.__setattr__(self, '_processed_at', datetime.utcnow())


test = {"a": 1}
t1 = Test(**test)
debug(t1)
t2 = TestExtra(**test)
debug(t2)
debug(t2._processed_at)
```
`_processed_at` gets added to the model, so it will be included in `.dict()` etc. I'm not sure whether that's what you want?

_(updated with a better solution)_

If not, you can do something like:

```py
from datetime import datetime

from pydantic import BaseModel


class Test(BaseModel):
    a: int


class TestExtra(BaseModel):
    __slots__ = ('processed_at',)
    a: int

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        object.__setattr__(self, 'processed_at', datetime.utcnow())


t2 = TestExtra(a=123)
debug(t2.dict())
debug(t2.processed_at)
```

outputs:
```
➤  python test.py 
test.py:20 <module>
    t2.dict(): {'a': 123} (dict) len=1
test.py:21 <module>
    t2.processed_at: datetime.datetime(2020, 1, 2, 19, 23, 33, 36215) (datetime)
```
Thanks for the fast answer, 

Indeed, private `processed_at` should not be included in `.dict()`, so the second solution you shared works fine. However it is painful (and hacky) to use `__slots__` and `object.__setattr__`, is there a limitation that cannot be overcome in the current implementation to have the following - natural behavior:

```
class TestExtra(BaseModel):
    a: int
    # Starts with underscore, private property
    # Won't be shown in .dict()
    _processed_at: datetime = None

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Some computation here
        self._processed_at = datetime.utcnow() # Won't throw ValueError
```

EDIT:
This comes from the fact that private properties are likely to be used for internal data-related operations: let's consider the following use-case:

I have a `Video(BaseModel)` class representing a video, that can be instantiated with a `path` property. I want this class to handle read operation on the video (to later extract frames from the video). I don't want to expose intermediate properties in the model, but I want to store in this instance a reference to the in-memory video object (using `CV2.VideoCapture`).

In that case, I would like something like that working:

```
class Video(BaseModel):
    path: Path
    _videoCapture: Any = None

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._videoCapture = cv.VideoCapture(self.path)
```
> is there a limitation that cannot be overcome in the current implementation to have the following - natural behavior:

Yes, the way we use `__dict__`.

Using slots is the only solution I know of, but the code is open source, maybe you can find another solution?
So after digging a bit into both code and doc, I tried with `extra=ignore`:

```
class TestExtra(BaseModel):
    a: int
    _private_key: bool = True

    class Config:
        extra = "ignore" # Default value

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Some computation here
        self._processed_at = datetime.utcnow()  # Won't throw ValueError
```

Then I bypassed the check in the code using:

```
    @no_type_check
    def __setattr__(self, name, value):
        if self.__config__.extra is Extra.forbid and name not in self.__fields__:
```
 instead of 
```
    @no_type_check
    def __setattr__(self, name, value):
        if self.__config__.extra is not Extra.allow and name not in self.__fields__:
```

Output:
```
{'a': 123, '_processed_at': datetime.datetime(2020, 1, 3, 12, 31, 20, 317079)}
```
Note that the output is still not what I want: `_processed_at` should be private and not included.

The doc is not very clear about this `extra` parameter behavior:

> extra :
    whether to ignore, allow, or forbid extra attributes during model initialization. Accepts the string values of 'ignore', 'allow', or 'forbid', or values of the Extra enum (default: Extra.ignore)

What is the scope of model initialization here? 

I am now looking for a `exclude_private: bool = True` in the `.dict()` method
> What is the scope of model initialization here?

`extra` will apply both when creating models and when setting attributes, you might also be able to use `validate_assignment=False` to avoid the assignment checks.

`exclude_private: bool = True` this doesn't exist, but you can use `exclude={'_processed_at'}` and even overwrite `dict()` on a custom base model to set it by default.
> exclude_private: bool = True this doesn't exist, but you can use exclude={'_processed_at'} and even overwrite dict() on a custom base model to set it by default.

Please check the PR above, where I implemented this parameter. WDYT ?

Commit message: add   # noqa: C901 (ignore complexity) to __setattr__
(see comment in PR)

Patch to review:
diff --git a/changes/1679-MrMrRobat.txt b/changes/1679-MrMrRobat.txt
new file mode 100644
index 00000000000..8c6a2dc2ebf
--- /dev/null
+++ b/changes/1679-MrMrRobat.txt
@@ -0,0 +1 @@
+Add private attributes support
\ No newline at end of file
diff --git a/docs/examples/private_attributes.py b/docs/examples/private_attributes.py
new file mode 100644
index 00000000000..ee1c0b4f962
--- /dev/null
+++ b/docs/examples/private_attributes.py
@@ -0,0 +1,18 @@
+from datetime import datetime
+from random import randint
+
+from pydantic import BaseModel, PrivateAttr
+
+
+class TimeAwareModel(BaseModel):
+    _processed_at: datetime = PrivateAttr(default_factory=datetime.now)
+    _secret_value: str = PrivateAttr()
+
+    def __init__(self, **data):
+        super().__init__(**data)
+        self._secret_value = randint(1, 5)
+
+
+m = TimeAwareModel()
+print(m._processed_at)
+print(m._secret_value)
diff --git a/docs/examples/private_attributes_underscore_attrs_are_private.py b/docs/examples/private_attributes_underscore_attrs_are_private.py
new file mode 100644
index 00000000000..ea7247ba267
--- /dev/null
+++ b/docs/examples/private_attributes_underscore_attrs_are_private.py
@@ -0,0 +1,16 @@
+from typing import ClassVar
+
+from pydantic import BaseModel
+
+
+class Model(BaseModel):
+    __class_var__: ClassVar[str] = 'class var value'
+    __private_attr__: str = 'private attr value'
+
+    class Config:
+        underscore_attrs_are_private = True
+
+
+print(Model.__class_var__)
+print(Model.__private_attr__)
+print(Model().__private_attr__)
diff --git a/docs/usage/models.md b/docs/usage/models.md
index c632af1fb05..d7689d13a7b 100644
--- a/docs/usage/models.md
+++ b/docs/usage/models.md
@@ -523,6 +523,24 @@ Where `Field` refers to the [field function](schema.md#field-customisation).
     Moreover if you want to validate default values with `validate_all`,
     *pydantic* will need to call the `default_factory`, which could lead to side effects!
 
+## Private model attributes
+
+If you need to use internal attribute excluded from model fields, you can declare them using `PrivateAttr`:
+
+```py
+{!.tmp_examples/private_attributes.py!}
+```
+_(This script is complete, it should run "as is")_
+
+If `Config.underscore_attrs_are_private` is `True`, any non-ClassVar underscore attribute will be treated as private:
+```py
+{!.tmp_examples/private_attributes_underscore_attrs_are_private.py!}
+```
+_(This script is complete, it should run "as is")_
+
+Upon class creation pydantic constructs `__slots__` filled with private attributes.
+Private attributes names must start with underscore to prevent conflicts with model fields.
+
 ## Parsing data into a specified type
 
 Pydantic includes a standalone utility function `parse_obj_as` that can be used to apply the parsing
diff --git a/pydantic/__init__.py b/pydantic/__init__.py
index 1a3cabb8c01..5cfc0583f74 100644
--- a/pydantic/__init__.py
+++ b/pydantic/__init__.py
@@ -5,7 +5,7 @@
 from .env_settings import BaseSettings
 from .error_wrappers import ValidationError
 from .errors import *
-from .fields import Field, Required, Schema
+from .fields import Field, PrivateAttr, Required, Schema
 from .main import *
 from .networks import *
 from .parse import Protocol
@@ -96,6 +96,7 @@
     'StrictInt',
     'StrictFloat',
     'PaymentCardNumber',
+    'PrivateAttr',
     'ByteSize',
     # version
     'VERSION',
diff --git a/pydantic/class_validators.py b/pydantic/class_validators.py
index c697ff52d9f..9cd951a58f2 100644
--- a/pydantic/class_validators.py
+++ b/pydantic/class_validators.py
@@ -7,7 +7,7 @@
 
 from .errors import ConfigError
 from .typing import AnyCallable
-from .utils import in_ipython
+from .utils import ROOT_KEY, in_ipython
 
 
 class Validator:
@@ -42,7 +42,6 @@ def __init__(
     ValidatorListDict = Dict[str, List[Validator]]
 
 _FUNCS: Set[str] = set()
-ROOT_KEY = '__root__'
 VALIDATOR_CONFIG_KEY = '__validator_config__'
 ROOT_VALIDATOR_CONFIG_KEY = '__root_validator_config__'
 
diff --git a/pydantic/fields.py b/pydantic/fields.py
index f2c9ba050ea..cbbf462a6c2 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -797,3 +797,31 @@ def __repr_args__(self) -> 'ReprArgs':
         if self.alt_alias:
             args.append(('alias', self.alias))
         return args
+
+
+class PrivateAttr(Representation):
+    """
+    Indicates that attribute is only used internally and never mixed with regular fields.
+
+    Types or values of private attrs are not checked by pydantic and it's up to you to keep them relevant.
+
+    Private attrs are stored in model __slots__.
+    """
+
+    __slots__ = ('default', 'default_factory')
+
+    def __init__(self, default: Any = Undefined, default_factory: Optional[NoArgAnyCallable] = None) -> None:
+        if default is not Undefined and default_factory is not None:
+            raise TypeError('default and default_factory args can not be used together')
+
+        self.default = default
+        self.default_factory = default_factory
+
+    def get_default(self) -> Any:
+        return smart_deepcopy(self.default) if self.default_factory is None else self.default_factory()
+
+    def __eq__(self, other: Any) -> bool:
+        return isinstance(other, self.__class__) and (self.default, self.default_factory) == (
+            other.default,
+            other.default_factory,
+        )
diff --git a/pydantic/main.py b/pydantic/main.py
index e55d61cb7de..1724c9e29db 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -16,6 +16,7 @@
     List,
     Mapping,
     Optional,
+    Set,
     Tuple,
     Type,
     TypeVar,
@@ -25,21 +26,24 @@
     overload,
 )
 
-from .class_validators import ROOT_KEY, ValidatorGroup, extract_root_validators, extract_validators, inherit_validators
+from .class_validators import ValidatorGroup, extract_root_validators, extract_validators, inherit_validators
 from .error_wrappers import ErrorWrapper, ValidationError
 from .errors import ConfigError, DictError, ExtraError, MissingError
-from .fields import SHAPE_MAPPING, ModelField, Undefined
+from .fields import SHAPE_MAPPING, ModelField, PrivateAttr, Undefined
 from .json import custom_pydantic_encoder, pydantic_encoder
 from .parse import Protocol, load_file, load_str_bytes
 from .schema import default_ref_template, model_schema
 from .types import PyObject, StrBytes
 from .typing import AnyCallable, ForwardRef, get_origin, is_classvar, resolve_annotations, update_field_forward_refs
 from .utils import (
+    ROOT_KEY,
     ClassAttribute,
     GetterDict,
     Representation,
     ValueItems,
     generate_model_signature,
+    is_valid_field,
+    is_valid_private_name,
     lenient_issubclass,
     sequence_like,
     smart_deepcopy,
@@ -81,7 +85,6 @@ def __call__(self, schema: Dict[str, Any], model_class: Type['Model']) -> None:
 else:
     SchemaExtraCallable = Callable[..., None]
 
-
 try:
     import cython  # type: ignore
 except ImportError:
@@ -123,6 +126,7 @@ class BaseConfig:
     json_loads: Callable[[str], Any] = json.loads
     json_dumps: Callable[..., str] = json.dumps
     json_encoders: Dict[Type[Any], AnyCallable] = {}
+    underscore_attrs_are_private = False
 
     @classmethod
     def get_field_info(cls, name: str) -> Dict[str, Any]:
@@ -189,12 +193,6 @@ def prepare_config(config: Type[BaseConfig], cls_name: str) -> None:
         config.case_sensitive = not config.case_insensitive  # type: ignore
 
 
-def is_valid_field(name: str) -> bool:
-    if not name.startswith('_'):
-        return True
-    return ROOT_KEY == name
-
-
 def validate_custom_root_type(fields: Dict[str, ModelField]) -> None:
     if len(fields) > 1:
         raise ValueError('__root__ cannot be mixed with other fields')
@@ -217,6 +215,10 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
         validators: 'ValidatorListDict' = {}
 
         pre_root_validators, post_root_validators = [], []
+        private_attributes: Dict[str, PrivateAttr] = {}
+        slots: Set[str] = namespace.get('__slots__', ())
+        slots = {slots} if isinstance(slots, str) else set(slots)
+
         for base in reversed(bases):
             if _is_base_model_class_defined and issubclass(base, BaseModel) and base != BaseModel:
                 fields.update(smart_deepcopy(base.__fields__))
@@ -224,6 +226,7 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
                 validators = inherit_validators(base.__validators__, validators)
                 pre_root_validators += base.__pre_root_validators__
                 post_root_validators += base.__post_root_validators__
+                private_attributes.update(base.__private_attributes__)
 
         config = inherit_config(namespace.get('Config'), config)
         validators = inherit_validators(extract_validators(namespace), validators)
@@ -263,14 +266,21 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
                         class_validators=vg.get_validators(ann_name),
                         config=config,
                     )
+                elif ann_name not in namespace and config.underscore_attrs_are_private:
+                    private_attributes[ann_name] = PrivateAttr()
 
             for var_name, value in namespace.items():
-                if (
-                    var_name not in annotations
-                    and is_valid_field(var_name)
-                    and not isinstance(value, untouched_types)
-                    and var_name not in class_vars
-                ):
+                can_be_changed = var_name not in class_vars and not isinstance(value, untouched_types)
+                if isinstance(value, PrivateAttr):
+                    if not is_valid_private_name(var_name):
+                        raise NameError(
+                            f'Private attributes "{var_name}" must not be a valid field name; '
+                            f'Use sunder or dunder names, e. g. "_{var_name}" or "__{var_name}__"'
+                        )
+                    private_attributes[var_name] = value
+                elif config.underscore_attrs_are_private and is_valid_private_name(var_name) and can_be_changed:
+                    private_attributes[var_name] = PrivateAttr(default=value)
+                elif is_valid_field(var_name) and var_name not in annotations and can_be_changed:
                     validate_field_name(bases, var_name)
                     inferred = ModelField.infer(
                         name=var_name,
@@ -296,6 +306,7 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
             json_encoder = pydantic_encoder
         pre_rv_new, post_rv_new = extract_root_validators(namespace)
 
+        exclude_from_namespace = fields | private_attributes.keys() | {'__slots__'}
         new_namespace = {
             '__config__': config,
             '__fields__': fields,
@@ -305,7 +316,9 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
             '__schema_cache__': {},
             '__json_encoder__': staticmethod(json_encoder),
             '__custom_root_type__': _custom_root_type,
-            **{n: v for n, v in namespace.items() if n not in fields},
+            '__private_attributes__': private_attributes,
+            '__slots__': slots | private_attributes.keys(),
+            **{n: v for n, v in namespace.items() if n not in exclude_from_namespace},
         }
 
         cls = super().__new__(mcs, name, bases, new_namespace, **kwargs)
@@ -314,6 +327,9 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
         return cls
 
 
+object_setattr = object.__setattr__
+
+
 class BaseModel(Representation, metaclass=ModelMetaclass):
     if TYPE_CHECKING:
         # populated by the metaclass, defined here to help IDEs only
@@ -327,6 +343,8 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
         __schema_cache__: 'DictAny' = {}
         __custom_root_type__: bool = False
         __signature__: 'Signature'
+        __private_attributes__: Dict[str, Any]
+        __fields_set__: SetStr = set()
 
     Config = BaseConfig
     __slots__ = ('__dict__', '__fields_set__')
@@ -339,17 +357,18 @@ def __init__(__pydantic_self__, **data: Any) -> None:
         Raises ValidationError if the input data cannot be parsed to form a valid model.
         """
         # Uses something other than `self` the first arg to allow "self" as a settable attribute
-        if TYPE_CHECKING:
-            __pydantic_self__.__dict__: Dict[str, Any] = {}
-            __pydantic_self__.__fields_set__: 'SetStr' = set()
         values, fields_set, validation_error = validate_model(__pydantic_self__.__class__, data)
         if validation_error:
             raise validation_error
-        object.__setattr__(__pydantic_self__, '__dict__', values)
-        object.__setattr__(__pydantic_self__, '__fields_set__', fields_set)
+        object_setattr(__pydantic_self__, '__dict__', values)
+        object_setattr(__pydantic_self__, '__fields_set__', fields_set)
+        __pydantic_self__._set_default_private_attributes(__pydantic_self__.__private_attributes__)
 
     @no_type_check
-    def __setattr__(self, name, value):
+    def __setattr__(self, name, value):  # noqa: C901 (ignore complexity)
+        if name in self.__private_attributes__:
+            return object_setattr(self, name, value)
+
         if self.__config__.extra is not Extra.allow and name not in self.__fields__:
             raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"')
         elif not self.__config__.allow_mutation:
@@ -385,11 +404,24 @@ def __setattr__(self, name, value):
         self.__fields_set__.add(name)
 
     def __getstate__(self) -> 'DictAny':
-        return {'__dict__': self.__dict__, '__fields_set__': self.__fields_set__}
+        return {
+            '__dict__': self.__dict__,
+            '__fields_set__': self.__fields_set__,
+            '__private_attributes_values__': {k: getattr(self, k, Undefined) for k in self.__private_attributes__},
+        }
 
     def __setstate__(self, state: 'DictAny') -> None:
-        object.__setattr__(self, '__dict__', state['__dict__'])
-        object.__setattr__(self, '__fields_set__', state['__fields_set__'])
+        object_setattr(self, '__dict__', state['__dict__'])
+        object_setattr(self, '__fields_set__', state['__fields_set__'])
+        for name, value in state.get('__private_attributes_values__', {}).items():
+            if value is not Undefined:
+                object_setattr(self, name, value)
+
+    def _set_default_private_attributes(self, source: Dict[str, PrivateAttr]) -> None:
+        for name, private_attr in source.items():
+            default = private_attr.get_default()
+            if default is not Undefined:
+                object_setattr(self, name, default)
 
     def dict(
         self,
@@ -528,8 +560,9 @@ def from_orm(cls: Type['Model'], obj: Any) -> 'Model':
         values, fields_set, validation_error = validate_model(cls, obj)
         if validation_error:
             raise validation_error
-        object.__setattr__(m, '__dict__', values)
-        object.__setattr__(m, '__fields_set__', fields_set)
+        object_setattr(m, '__dict__', values)
+        object_setattr(m, '__fields_set__', fields_set)
+        m._set_default_private_attributes(cls.__private_attributes__)
         return m
 
     @classmethod
@@ -542,10 +575,11 @@ def construct(cls: Type['Model'], _fields_set: Optional['SetStr'] = None, **valu
         # default field values
         fields_values = {name: field.get_default() for name, field in cls.__fields__.items() if not field.required}
         fields_values.update(values)
-        object.__setattr__(m, '__dict__', fields_values)
+        object_setattr(m, '__dict__', fields_values)
         if _fields_set is None:
             _fields_set = set(values.keys())
-        object.__setattr__(m, '__fields_set__', _fields_set)
+        object_setattr(m, '__fields_set__', _fields_set)
+        m._set_default_private_attributes(cls.__private_attributes__)
         return m
 
     def copy(
@@ -578,8 +612,9 @@ def copy(
 
         cls = self.__class__
         m = cls.__new__(cls)
-        object.__setattr__(m, '__dict__', v)
-        object.__setattr__(m, '__fields_set__', self.__fields_set__.copy())
+        object_setattr(m, '__dict__', v)
+        object_setattr(m, '__fields_set__', self.__fields_set__.copy())
+        m._set_default_private_attributes(cls.__private_attributes__)
         return m
 
     @classmethod
diff --git a/pydantic/schema.py b/pydantic/schema.py
index 079e08c1970..be2b454ce37 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -25,7 +25,6 @@
 )
 from uuid import UUID
 
-from .class_validators import ROOT_KEY
 from .fields import (
     SHAPE_FROZENSET,
     SHAPE_ITERABLE,
@@ -59,7 +58,7 @@
     constr,
 )
 from .typing import ForwardRef, Literal, get_args, get_origin, is_callable_type, is_literal_type, literal_values
-from .utils import get_model, lenient_issubclass, sequence_like
+from .utils import ROOT_KEY, get_model, lenient_issubclass, sequence_like
 
 if TYPE_CHECKING:
     from .dataclasses import DataclassType  # noqa: F401
diff --git a/pydantic/utils.py b/pydantic/utils.py
index e67f336c1f0..c9b0f3c92fc 100644
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -45,14 +45,18 @@
     'almost_equal_floats',
     'get_model',
     'to_camel',
+    'is_valid_field',
+    'smart_deepcopy',
     'PyObjectStr',
     'Representation',
     'GetterDict',
     'ValueItems',
     'version_info',  # required here to match behaviour in v1.3
     'ClassAttribute',
+    'ROOT_KEY',
 )
 
+ROOT_KEY = '__root__'
 # these are types that are returned unchanged by deepcopy
 IMMUTABLE_NON_COLLECTIONS_TYPES: Set[Type[Any]] = {
     int,
@@ -591,3 +595,13 @@ def smart_deepcopy(obj: Obj) -> Obj:
         # faster way for empty collections, no need to copy its members
         return obj if obj_type is tuple else obj.copy()  # type: ignore  # tuple doesn't have copy method
     return deepcopy(obj)  # slowest way when we actually might need a deepcopy
+
+
+def is_valid_field(name: str) -> bool:
+    if not name.startswith('_'):
+        return True
+    return ROOT_KEY == name
+
+
+def is_valid_private_name(name: str) -> bool:
+    return not is_valid_field(name) and name not in {'__annotations__', '__module__', '__annotations__', '__qualname__'}
diff --git a/tests/test_construction.py b/tests/test_construction.py
index 93abb6e032f..ecfa60d80ff 100644
--- a/tests/test_construction.py
+++ b/tests/test_construction.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, PrivateAttr
 
 
 class Model(BaseModel):
@@ -58,6 +58,8 @@ def test_simple_copy():
 
 
 class ModelTwo(BaseModel):
+    __foo__ = PrivateAttr({'private'})
+
     a: float
     b: int = 10
     c: str = 'foobar'
@@ -74,6 +76,8 @@ def test_deep_copy():
     assert m.d is not m2.d
     assert m == m2
     assert m.__fields__ == m2.__fields__
+    assert m.__foo__ == m2.__foo__
+    assert m.__foo__ is not m2.__foo__
 
 
 def test_copy_exclude():
@@ -215,6 +219,7 @@ def test_recursive_pickle():
     assert m.d.a == 123.45
     assert m2.d.a == 123.45
     assert m.__fields__ == m2.__fields__
+    assert m.__foo__ == m2.__foo__
 
 
 def test_immutable_copy():
diff --git a/tests/test_main.py b/tests/test_main.py
index f7ad7ee0590..002a6f8889f 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -12,6 +12,7 @@
     Field,
     NoneBytes,
     NoneStr,
+    PrivateAttr,
     Required,
     ValidationError,
     constr,
@@ -1374,3 +1375,96 @@ class M(BaseModel):
         a: int
 
     get_type_hints(M.__config__)
+
+
+def test_private_attribute():
+    default = {'a': {}}
+
+    class Model(BaseModel):
+        __foo__ = PrivateAttr(default)
+
+    assert Model.__slots__ == {'__foo__'}
+    assert repr(Model.__foo__) == "<member '__foo__' of 'Model' objects>"
+    assert Model.__private_attributes__ == {'__foo__': PrivateAttr(default)}
+
+    m = Model()
+    assert m.__foo__ == default
+    assert m.__foo__ is not default
+    assert m.__foo__['a'] is not default['a']
+
+    m.__foo__ = None
+    assert m.__foo__ is None
+
+    assert m.dict() == {}
+    assert m.__dict__ == {}
+
+
+def test_underscore_attrs_are_private():
+    class Model(BaseModel):
+        __foo__: str = 'abc'
+        __bar__: ClassVar[str] = 'cba'
+
+        class Config:
+            underscore_attrs_are_private = True
+
+    assert Model.__slots__ == {'__foo__'}
+    assert repr(Model.__foo__) == "<member '__foo__' of 'Model' objects>"
+    assert Model.__bar__ == 'cba'
+    assert Model.__private_attributes__ == {'__foo__': PrivateAttr('abc')}
+
+    m = Model()
+    assert m.__foo__ == 'abc'
+    m.__foo__ = None
+    assert m.__foo__ is None
+
+    with pytest.raises(ValueError, match='"Model" object has no field "__bar__"'):
+        m.__bar__ = 1
+
+
+def test_private_attribute_intersection_with_extra_field():
+    class Model(BaseModel):
+        __foo__ = PrivateAttr('private_attribute')
+
+        class Config:
+            extra = Extra.allow
+
+    assert Model.__slots__ == {'__foo__'}
+    m = Model(__foo__='field')
+    assert m.__foo__ == 'private_attribute'
+    assert m.__dict__ == m.dict() == {'__foo__': 'field'}
+
+    m.__foo__ = 'still_private'
+    assert m.__foo__ == 'still_private'
+    assert m.__dict__ == m.dict() == {'__foo__': 'field'}
+
+
+def test_private_attribute_invalid_name():
+    with pytest.raises(
+        NameError,
+        match='Private attributes "foo" must not be a valid field name; '
+        'Use sunder or dunder names, e. g. "_foo" or "__foo__"',
+    ):
+
+        class Model(BaseModel):
+            foo = PrivateAttr()
+
+
+def test_slots_are_ignored():
+    class Model(BaseModel):
+        __slots__ = (
+            'foo',
+            '_bar',
+        )
+
+        def __init__(self):
+            super().__init__()
+            for attr_ in self.__slots__:
+                object.__setattr__(self, attr_, 'spam')
+
+    assert Model.__private_attributes__ == {}
+    assert set(Model.__slots__) == {'foo', '_bar'}
+    m = Model()
+    for attr in Model.__slots__:
+        assert object.__getattribute__(m, attr) == 'spam'
+        with pytest.raises(ValueError, match=f'"Model" object has no field "{attr}"'):
+            setattr(m, attr, 'not spam')


Code review comments:
diff_hunk: @@ -123,6 +126,7 @@ class BaseConfig:
     json_loads: Callable[[str], Any] = json.loads
     json_dumps: Callable[..., str] = json.dumps
     json_encoders: Dict[Type[Any], AnyCallable] = {}
+    underscore_attrs_are_private = False
path: pydantic/main.py
text: @user1:
```suggestion
    underscore_attrs_are_private: bool = False
```
I know it's obvious, but best to be explicit.

diff_hunk: @@ -339,17 +357,18 @@ def __init__(__pydantic_self__, **data: Any) -> None:
         Raises ValidationError if the input data cannot be parsed to form a valid model.
         """
         # Uses something other than `self` the first arg to allow "self" as a settable attribute
-        if TYPE_CHECKING:
-            __pydantic_self__.__dict__: Dict[str, Any] = {}
-            __pydantic_self__.__fields_set__: 'SetStr' = set()
         values, fields_set, validation_error = validate_model(__pydantic_self__.__class__, data)
         if validation_error:
             raise validation_error
-        object.__setattr__(__pydantic_self__, '__dict__', values)
-        object.__setattr__(__pydantic_self__, '__fields_set__', fields_set)
+        object_setattr(__pydantic_self__, '__dict__', values)
+        object_setattr(__pydantic_self__, '__fields_set__', fields_set)
+        __pydantic_self__._set_default_private_attributes(__pydantic_self__.__private_attributes__)
path: pydantic/main.py
text: @user1:
why is this required?

@author:
what do you mean exactly? 

@user1:
Can we avoid this line since this is the critical path in terms of performance?

@author:
It sets defaults of `__private_attributes__`, without this line `default` not `default_factoru` wouldn't work.
What's critical here in terms of performance? Function call itself or executing code of the function?

@user1:
Ok, I guess we need it.

Initialising/validating models is the critical path, I don't want to slow that down unless we absolutely have to.